Starchik1 commited on
Commit
bcd5aaa
·
verified ·
1 Parent(s): 7aeb84b

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +579 -249
main.py CHANGED
@@ -15,6 +15,7 @@ socketio = SocketIO(app, cors_allowed_origins="*")
15
  users = {}
16
  messages = []
17
  voice_messages = {}
 
18
 
19
  HTML_TEMPLATE = '''
20
  <!DOCTYPE html>
@@ -33,7 +34,7 @@ HTML_TEMPLATE = '''
33
 
34
  body {
35
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
36
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
37
  height: 100vh;
38
  overflow: hidden;
39
  }
@@ -45,7 +46,6 @@ HTML_TEMPLATE = '''
45
  max-width: 100%;
46
  margin: 0 auto;
47
  background: white;
48
- box-shadow: 0 0 20px rgba(0,0,0,0.1);
49
  }
50
 
51
  /* Login Screen */
@@ -77,6 +77,7 @@ HTML_TEMPLATE = '''
77
  border-radius: 25px;
78
  font-size: 16px;
79
  background: rgba(255,255,255,0.9);
 
80
  }
81
 
82
  .login-btn {
@@ -103,17 +104,43 @@ HTML_TEMPLATE = '''
103
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
104
  color: white;
105
  padding: 15px;
106
- text-align: center;
107
  position: relative;
 
 
 
108
  }
109
 
110
- .online-users {
111
- background: rgba(255,255,255,0.2);
112
- padding: 10px;
113
- margin-top: 10px;
114
- border-radius: 15px;
 
 
 
 
 
115
  font-size: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  cursor: pointer;
 
 
 
117
  }
118
 
119
  .messages-container {
@@ -128,7 +155,7 @@ HTML_TEMPLATE = '''
128
  margin: 10px 0;
129
  padding: 12px 15px;
130
  border-radius: 18px;
131
- max-width: 80%;
132
  word-wrap: break-word;
133
  position: relative;
134
  animation: fadeIn 0.3s ease-in;
@@ -171,7 +198,7 @@ HTML_TEMPLATE = '''
171
  .voice-message {
172
  display: flex;
173
  align-items: center;
174
- padding: 8px 15px;
175
  }
176
 
177
  .voice-play-btn {
@@ -209,6 +236,7 @@ HTML_TEMPLATE = '''
209
  margin: 0 5px;
210
  font-size: 16px;
211
  outline: none;
 
212
  }
213
 
214
  .action-btn {
@@ -221,42 +249,196 @@ HTML_TEMPLATE = '''
221
  transition: background 0.2s;
222
  min-width: 44px;
223
  min-height: 44px;
 
224
  }
225
 
226
  .action-btn:active {
227
  background: rgba(0,0,0,0.1);
228
  }
229
 
230
- .call-buttons {
 
 
 
 
 
 
 
 
 
 
231
  display: flex;
232
- justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  padding: 10px;
234
- background: white;
235
- border-top: 1px solid #e0e0e0;
236
  }
237
 
238
- .call-btn {
239
- background: #4CAF50;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  border: none;
242
  border-radius: 50%;
243
- width: 60px;
244
- height: 60px;
245
- margin: 0 15px;
246
  font-size: 24px;
247
  cursor: pointer;
248
- box-shadow: 0 4px 8px rgba(0,0,0,0.2);
249
- transition: transform 0.2s;
 
 
250
  }
251
 
252
- .call-btn:active {
253
- transform: scale(0.95);
254
  }
255
 
256
- .video-call-btn {
257
- background: #FF5722;
258
  }
259
 
 
260
  .recording-overlay {
261
  position: fixed;
262
  top: 0;
@@ -287,49 +469,61 @@ HTML_TEMPLATE = '''
287
  100% { transform: scale(0.8); opacity: 1; }
288
  }
289
 
290
- .user-list {
291
- position: absolute;
292
- top: 100%;
 
293
  left: 0;
294
- right: 0;
295
- background: white;
296
- border: 1px solid #ddd;
297
- border-radius: 10px;
298
- margin: 5px;
299
  display: none;
300
- z-index: 100;
301
- max-height: 200px;
302
- overflow-y: auto;
303
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
304
  }
305
 
306
- .user-item {
307
- padding: 12px 15px;
308
- border-bottom: 1px solid #eee;
309
- cursor: pointer;
310
- color: #333;
 
 
311
  }
312
 
313
- .user-item:last-child {
314
- border-bottom: none;
 
 
315
  }
316
 
317
- .user-item:active {
318
- background: #f0f0f0;
 
 
 
 
 
319
  }
320
 
321
- .selected-user {
322
- margin-top: 10px;
323
- font-size: 14px;
324
- background: rgba(255,255,255,0.3);
325
- padding: 8px 12px;
326
- border-radius: 15px;
327
  }
328
 
329
- .call-info {
330
- margin-top: 5px;
331
- font-size: 12px;
332
- opacity: 0.9;
 
 
 
 
 
 
 
 
333
  }
334
  </style>
335
  </head>
@@ -350,15 +544,15 @@ HTML_TEMPLATE = '''
350
  <!-- Main Chat Screen -->
351
  <div id="chatScreen" class="app-container chat-screen">
352
  <div class="header">
353
- <h2>💬 Мессенджер</h2>
354
- <div class="online-users" onclick="toggleUserList()">
355
- Онлайн: <span id="onlineCount">0</span> пользователей
356
- <div class="call-info">Нажмите для выбора</div>
 
357
  </div>
358
- <div id="selectedUserInfo" class="selected-user" style="display: none;">
359
- Выбрано: <span id="selectedUserName">-</span>
360
  </div>
361
- <div id="userList" class="user-list"></div>
362
  </div>
363
 
364
  <div id="messagesContainer" class="messages-container">
@@ -367,18 +561,43 @@ HTML_TEMPLATE = '''
367
  </div>
368
  </div>
369
 
370
- <div class="call-buttons">
371
- <button class="call-btn" onclick="startAudioCall()" title="Аудиозвонок">📞</button>
372
- <button class="call-btn video-call-btn" onclick="startVideoCall()" title="Видеозвонок">📹</button>
373
- </div>
374
-
375
  <div class="input-area">
376
- <button class="action-btn" onclick="toggleVoiceRecord()" title="Голосовое сообщение">🎤</button>
377
  <input type="text" id="messageInput" class="message-input" placeholder="Введите сообщение..." maxlength="500">
378
  <button class="action-btn" onclick="sendMessage()" title="Отправить">📤</button>
379
  </div>
380
  </div>
381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  <!-- Recording Overlay -->
383
  <div id="recordingOverlay" class="recording-overlay">
384
  <div class="recording-animation"></div>
@@ -386,6 +605,18 @@ HTML_TEMPLATE = '''
386
  <p>Нажмите для остановки</p>
387
  </div>
388
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  <script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
390
  <script>
391
  const socket = io();
@@ -397,8 +628,10 @@ HTML_TEMPLATE = '''
397
  let remoteStream = null;
398
  let peerConnection = null;
399
  let currentCall = null;
400
- let selectedUser = null;
401
- let onlineUsers = [];
 
 
402
 
403
  // WebRTC configuration
404
  const rtcConfig = {
@@ -421,8 +654,7 @@ HTML_TEMPLATE = '''
421
 
422
  socket.on('user_list_update', (data) => {
423
  document.getElementById('onlineCount').textContent = data.count;
424
- onlineUsers = data.users || [];
425
- updateUserList();
426
  });
427
 
428
  socket.on('new_message', (message) => {
@@ -434,32 +666,23 @@ HTML_TEMPLATE = '''
434
  });
435
 
436
  socket.on('incoming_call', (data) => {
437
- if (confirm(`Входящий ${data.isVideo ? 'видео' : 'аудио'} звонок от ${data.from_user}. Принять?`)) {
438
- socket.emit('call_answer', {
439
- call_id: data.call_id,
440
- answer: true,
441
- to_user: data.from_user,
442
- isVideo: data.isVideo
443
- });
444
- startCall(data.isVideo, false, data.from_user);
445
- } else {
446
- socket.emit('call_answer', {
447
- call_id: data.call_id,
448
- answer: false,
449
- to_user: data.from_user
450
- });
451
- }
452
  });
453
 
454
  socket.on('call_answered', (data) => {
455
  if (data.answer) {
456
- startCall(data.isVideo, true, data.from_user);
457
  } else {
458
- alert('Звонок отклонен');
459
- endCall();
460
  }
461
  });
462
 
 
 
 
 
 
463
  socket.on('webrtc_offer', async (data) => {
464
  if (!peerConnection) {
465
  await createPeerConnection(false);
@@ -467,9 +690,11 @@ HTML_TEMPLATE = '''
467
  await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
468
  const answer = await peerConnection.createAnswer();
469
  await peerConnection.setLocalDescription(answer);
 
470
  socket.emit('webrtc_answer', {
471
  answer: answer,
472
- to_user: data.from_user
 
473
  });
474
  });
475
 
@@ -522,7 +747,7 @@ HTML_TEMPLATE = '''
522
  container.appendChild(messageDiv);
523
  container.scrollTop = container.scrollHeight;
524
 
525
- // Убираем placeholder если есть сообщения
526
  const placeholder = container.querySelector('div[style*="text-align: center"]');
527
  if (placeholder) {
528
  placeholder.remove();
@@ -550,55 +775,45 @@ HTML_TEMPLATE = '''
550
  container.scrollTop = container.scrollHeight;
551
  }
552
 
553
- function updateUserList() {
554
- const userList = document.getElementById('userList');
555
- userList.innerHTML = '';
 
 
556
 
557
- if (onlineUsers.length === 0) {
558
- const emptyItem = document.createElement('div');
559
- emptyItem.className = 'user-item';
560
- emptyItem.textContent = 'Нет других пользователей онлайн';
561
- emptyItem.style.color = '#999';
562
- emptyItem.style.cursor = 'default';
563
- userList.appendChild(emptyItem);
564
  return;
565
  }
566
 
567
- onlineUsers.forEach(user => {
568
- if (user !== currentUser.username) {
569
- const userItem = document.createElement('div');
570
- userItem.className = 'user-item';
571
- userItem.textContent = user;
572
- userItem.onclick = () => {
573
- selectedUser = user;
574
- document.getElementById('selectedUserName').textContent = user;
575
- document.getElementById('selectedUserInfo').style.display = 'block';
576
- userList.style.display = 'none';
577
- };
578
- userList.appendChild(userItem);
579
- }
580
  });
581
  }
582
 
583
- function toggleUserList() {
584
- const userList = document.getElementById('userList');
585
- if (onlineUsers.length <= 1) {
586
- alert('Нет других пользователей онлайн');
587
- return;
588
- }
589
- userList.style.display = userList.style.display === 'block' ? 'none' : 'block';
590
  }
591
 
592
  // Voice Messages
593
- async function toggleVoiceRecord() {
594
- if (!isRecording) {
595
- await startRecording();
596
- } else {
597
  stopRecording();
 
598
  }
599
- }
600
 
601
- async function startRecording() {
602
  try {
603
  const stream = await navigator.mediaDevices.getUserMedia({
604
  audio: {
@@ -642,7 +857,7 @@ HTML_TEMPLATE = '''
642
  mediaRecorder.start();
643
  isRecording = true;
644
 
645
- // Показываем оверлей записи
646
  document.getElementById('recordingOverlay').style.display = 'flex';
647
 
648
  } catch (error) {
@@ -664,145 +879,240 @@ HTML_TEMPLATE = '''
664
  audio.play().catch(e => console.error('Error playing audio:', e));
665
  }
666
 
667
- // WebRTC Functions
668
- async function createPeerConnection(isCaller) {
669
- try {
670
- peerConnection = new RTCPeerConnection(rtcConfig);
671
-
672
- // Add local stream if available
673
- if (localStream) {
674
- localStream.getTracks().forEach(track => {
675
- peerConnection.addTrack(track, localStream);
676
- });
677
- }
678
-
679
- // Handle incoming tracks
680
- peerConnection.ontrack = (event) => {
681
- remoteStream = event.streams[0];
682
- // Можно добавить отображение удаленного видео
683
- console.log('Remote track received');
684
- };
685
-
686
- // Handle ICE candidates
687
- peerConnection.onicecandidate = (event) => {
688
- if (event.candidate && selectedUser) {
689
- socket.emit('webrtc_ice_candidate', {
690
- candidate: event.candidate,
691
- to_user: selectedUser
692
- });
693
- }
694
- };
695
 
696
- return peerConnection;
697
- } catch (error) {
698
- console.error('Error creating peer connection:', error);
699
- throw error;
700
- }
 
 
 
 
 
701
  }
702
 
703
- async function startAudioCall() {
704
- if (!selectedUser) {
705
- alert('Сначала выберите пользователя из списка онл��йн (нажмите на "Онлайн: X пользователей")');
706
- toggleUserList();
707
- return;
708
- }
709
- await startCall(false, true, selectedUser);
 
 
 
710
  }
711
 
712
- async function startVideoCall() {
713
- if (!selectedUser) {
714
- alert('Сначала выберите пользователя из списка онлайн (нажмите на "Онлайн: X пользователей")');
715
- toggleUserList();
716
- return;
717
- }
718
- await startCall(true, true, selectedUser);
 
 
719
  }
720
 
721
- async function startCall(isVideo, isCaller, targetUser) {
722
  try {
723
- selectedUser = targetUser;
724
 
725
  // Get local media stream
726
  const constraints = {
727
  audio: true,
728
  video: isVideo ? {
729
- width: { ideal: 1280 },
730
- height: { ideal: 720 }
 
731
  } : false
732
  };
733
 
734
  localStream = await navigator.mediaDevices.getUserMedia(constraints);
 
735
 
736
  // Create peer connection
737
- await createPeerConnection(isCaller);
738
 
739
- if (isCaller) {
740
  // Create and send offer
741
  const offer = await peerConnection.createOffer();
742
  await peerConnection.setLocalDescription(offer);
743
 
744
  socket.emit('webrtc_offer', {
745
  offer: offer,
746
- to_user: targetUser,
747
- isVideo: isVideo
748
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
 
750
- // Send call request
751
- socket.emit('call_request', {
752
- call_id: Date.now().toString(),
753
- from_user: currentUser.username,
754
- to_user: targetUser,
755
- isVideo: isVideo
756
  });
757
  }
 
758
 
759
- alert(`${isVideo ? 'Видео' : 'Аудио'} звонок ${isCaller ? 'начат' : 'принят'} с ${targetUser}`);
 
 
 
 
 
 
760
 
761
- } catch (error) {
762
- console.error('Error starting call:', error);
763
- alert(`Ошибка ${isVideo ? 'видео' : 'аудио'} звонка: ${error.message}`);
764
- endCall();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
  }
766
  }
767
 
 
 
 
 
 
 
 
 
 
 
 
 
768
  function endCall() {
769
  if (peerConnection) {
770
  peerConnection.close();
771
  peerConnection = null;
772
  }
 
773
  if (localStream) {
774
  localStream.getTracks().forEach(track => track.stop());
775
  localStream = null;
776
  }
777
- remoteStream = null;
 
 
 
 
 
 
 
 
778
  currentCall = null;
 
 
779
  }
780
 
781
- // Touch events for voice recording
782
- let recordingTimer;
783
- const voiceBtn = document.querySelector('.action-btn[title="Голосовое сообщение"]');
784
-
785
- if (voiceBtn) {
786
- voiceBtn.addEventListener('touchstart', function(e) {
787
- e.preventDefault();
788
- recordingTimer = setTimeout(() => {
789
- startRecording();
790
- }, 500);
791
- });
792
 
793
- voiceBtn.addEventListener('touchend', function(e) {
794
- e.preventDefault();
795
- clearTimeout(recordingTimer);
796
- if (isRecording) {
797
- stopRecording();
 
 
 
 
 
 
798
  }
799
- });
800
  }
801
 
802
- // Click on recording overlay to stop
803
  document.getElementById('recordingOverlay').addEventListener('click', stopRecording);
804
-
805
- // Keyboard events
806
  document.getElementById('messageInput').addEventListener('keypress', (e) => {
807
  if (e.key === 'Enter') {
808
  sendMessage();
@@ -815,17 +1125,15 @@ HTML_TEMPLATE = '''
815
  }
816
  });
817
 
818
- // Close user list when clicking outside
819
  document.addEventListener('click', (e) => {
820
- if (!e.target.closest('.online-users') && !e.target.closest('.user-list')) {
821
- document.getElementById('userList').style.display = 'none';
 
 
 
822
  }
823
  });
824
-
825
- // Initialize
826
- window.onload = function() {
827
- onlineUsers = [];
828
- };
829
  </script>
830
  </body>
831
  </html>
@@ -838,7 +1146,6 @@ def index():
838
  @socketio.on('connect')
839
  def handle_connect():
840
  print(f'Client connected: {request.sid}')
841
- # Send current user list to the newly connected client
842
  emit('user_list_update', {
843
  'count': len(users),
844
  'users': [user_data['username'] for user_data in users.values()]
@@ -848,6 +1155,12 @@ def handle_connect():
848
  def handle_disconnect():
849
  for user_id, user_data in list(users.items()):
850
  if user_data.get('sid') == request.sid:
 
 
 
 
 
 
851
  del users[user_id]
852
  break
853
 
@@ -909,15 +1222,23 @@ def handle_voice_message(data):
909
 
910
  @socketio.on('call_request')
911
  def handle_call_request(data):
912
- target_user = data.get('to_user')
913
  target_sid = None
914
 
915
  for user_data in users.values():
916
- if user_data['username'] == target_user:
917
  target_sid = user_data['sid']
918
  break
919
 
920
  if target_sid:
 
 
 
 
 
 
 
 
921
  emit('incoming_call', {
922
  'call_id': data['call_id'],
923
  'from_user': data['from_user'],
@@ -926,44 +1247,53 @@ def handle_call_request(data):
926
 
927
  @socketio.on('call_answer')
928
  def handle_call_answer(data):
929
- target_user = data.get('to_user')
930
- target_sid = None
931
-
932
- for user_data in users.values():
933
- if user_data['username'] == target_user:
934
- target_sid = user_data['sid']
935
- break
936
-
937
- if target_sid:
938
- emit('call_answered', {
939
- 'answer': data['answer'],
940
- 'from_user': data.get('from_user', current_user),
941
- 'isVideo': data.get('isVideo', False)
942
- }, room=target_sid)
 
 
 
 
 
 
 
 
943
 
944
  @socketio.on('webrtc_offer')
945
  def handle_webrtc_offer(data):
946
- target_user = data.get('to_user')
947
  target_sid = None
948
 
949
  for user_data in users.values():
950
- if user_data['username'] == target_user:
951
  target_sid = user_data['sid']
952
  break
953
 
954
  if target_sid:
955
  emit('webrtc_offer', {
956
  'offer': data['offer'],
957
- 'from_user': data.get('from_user')
 
958
  }, room=target_sid)
959
 
960
  @socketio.on('webrtc_answer')
961
  def handle_webrtc_answer(data):
962
- target_user = data.get('to_user')
963
  target_sid = None
964
 
965
  for user_data in users.values():
966
- if user_data['username'] == target_user:
967
  target_sid = user_data['sid']
968
  break
969
 
@@ -975,11 +1305,11 @@ def handle_webrtc_answer(data):
975
 
976
  @socketio.on('webrtc_ice_candidate')
977
  def handle_webrtc_ice_candidate(data):
978
- target_user = data.get('to_user')
979
  target_sid = None
980
 
981
  for user_data in users.values():
982
- if user_data['username'] == target_user:
983
  target_sid = user_data['sid']
984
  break
985
 
 
15
  users = {}
16
  messages = []
17
  voice_messages = {}
18
+ active_calls = {}
19
 
20
  HTML_TEMPLATE = '''
21
  <!DOCTYPE html>
 
34
 
35
  body {
36
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
37
+ background: #f0f2f5;
38
  height: 100vh;
39
  overflow: hidden;
40
  }
 
46
  max-width: 100%;
47
  margin: 0 auto;
48
  background: white;
 
49
  }
50
 
51
  /* Login Screen */
 
77
  border-radius: 25px;
78
  font-size: 16px;
79
  background: rgba(255,255,255,0.9);
80
+ text-align: center;
81
  }
82
 
83
  .login-btn {
 
104
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
105
  color: white;
106
  padding: 15px;
 
107
  position: relative;
108
+ display: flex;
109
+ justify-content: space-between;
110
+ align-items: center;
111
  }
112
 
113
+ .header-info {
114
+ flex: 1;
115
+ }
116
+
117
+ .header h2 {
118
+ margin-bottom: 5px;
119
+ font-size: 18px;
120
+ }
121
+
122
+ .online-info {
123
  font-size: 14px;
124
+ opacity: 0.9;
125
+ }
126
+
127
+ .call-buttons {
128
+ display: flex;
129
+ gap: 10px;
130
+ }
131
+
132
+ .header-btn {
133
+ background: rgba(255,255,255,0.2);
134
+ border: none;
135
+ border-radius: 50%;
136
+ width: 44px;
137
+ height: 44px;
138
+ color: white;
139
+ font-size: 18px;
140
  cursor: pointer;
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: center;
144
  }
145
 
146
  .messages-container {
 
155
  margin: 10px 0;
156
  padding: 12px 15px;
157
  border-radius: 18px;
158
+ max-width: 85%;
159
  word-wrap: break-word;
160
  position: relative;
161
  animation: fadeIn 0.3s ease-in;
 
198
  .voice-message {
199
  display: flex;
200
  align-items: center;
201
+ padding: 8px 0;
202
  }
203
 
204
  .voice-play-btn {
 
236
  margin: 0 5px;
237
  font-size: 16px;
238
  outline: none;
239
+ background: #f8f9fa;
240
  }
241
 
242
  .action-btn {
 
249
  transition: background 0.2s;
250
  min-width: 44px;
251
  min-height: 44px;
252
+ color: #007AFF;
253
  }
254
 
255
  .action-btn:active {
256
  background: rgba(0,0,0,0.1);
257
  }
258
 
259
+ /* Users List */
260
+ .users-sidebar {
261
+ position: fixed;
262
+ top: 0;
263
+ right: -300px;
264
+ width: 300px;
265
+ height: 100%;
266
+ background: white;
267
+ box-shadow: -2px 0 10px rgba(0,0,0,0.1);
268
+ transition: right 0.3s ease;
269
+ z-index: 1000;
270
  display: flex;
271
+ flex-direction: column;
272
+ }
273
+
274
+ .users-sidebar.open {
275
+ right: 0;
276
+ }
277
+
278
+ .sidebar-header {
279
+ padding: 20px;
280
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
281
+ color: white;
282
+ display: flex;
283
+ justify-content: space-between;
284
+ align-items: center;
285
+ }
286
+
287
+ .close-sidebar {
288
+ background: none;
289
+ border: none;
290
+ color: white;
291
+ font-size: 20px;
292
+ cursor: pointer;
293
+ }
294
+
295
+ .users-list {
296
+ flex: 1;
297
+ overflow-y: auto;
298
  padding: 10px;
 
 
299
  }
300
 
301
+ .user-item {
302
+ display: flex;
303
+ align-items: center;
304
+ padding: 15px;
305
+ border-bottom: 1px solid #eee;
306
+ cursor: pointer;
307
+ }
308
+
309
+ .user-item:active {
310
+ background: #f5f5f5;
311
+ }
312
+
313
+ .user-avatar {
314
+ width: 40px;
315
+ height: 40px;
316
+ border-radius: 50%;
317
+ background: #007AFF;
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: center;
321
+ color: white;
322
+ font-weight: bold;
323
+ margin-right: 12px;
324
+ }
325
+
326
+ .user-info {
327
+ flex: 1;
328
+ }
329
+
330
+ .user-name {
331
+ font-weight: 500;
332
+ margin-bottom: 2px;
333
+ }
334
+
335
+ .user-status {
336
+ font-size: 12px;
337
+ color: #4CAF50;
338
+ }
339
+
340
+ .call-icon {
341
+ background: none;
342
+ border: none;
343
+ font-size: 20px;
344
+ cursor: pointer;
345
+ padding: 8px;
346
+ color: #007AFF;
347
+ }
348
+
349
+ /* Call Screen */
350
+ .call-screen {
351
+ position: fixed;
352
+ top: 0;
353
+ left: 0;
354
+ width: 100%;
355
+ height: 100%;
356
+ background: #1a1a1a;
357
+ z-index: 2000;
358
+ display: none;
359
+ flex-direction: column;
360
+ }
361
+
362
+ .video-container {
363
+ flex: 1;
364
+ position: relative;
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ }
369
+
370
+ .remote-video {
371
+ width: 100%;
372
+ height: 100%;
373
+ object-fit: cover;
374
+ }
375
+
376
+ .local-video {
377
+ position: absolute;
378
+ bottom: 20px;
379
+ right: 20px;
380
+ width: 120px;
381
+ height: 160px;
382
+ border-radius: 10px;
383
+ border: 2px solid white;
384
+ object-fit: cover;
385
+ }
386
+
387
+ .call-info {
388
+ position: absolute;
389
+ top: 40px;
390
+ left: 0;
391
+ right: 0;
392
+ text-align: center;
393
  color: white;
394
+ z-index: 10;
395
+ }
396
+
397
+ .call-status {
398
+ font-size: 18px;
399
+ margin-bottom: 5px;
400
+ }
401
+
402
+ .call-timer {
403
+ font-size: 24px;
404
+ font-weight: bold;
405
+ }
406
+
407
+ .call-controls {
408
+ position: absolute;
409
+ bottom: 40px;
410
+ left: 0;
411
+ right: 0;
412
+ display: flex;
413
+ justify-content: center;
414
+ gap: 20px;
415
+ z-index: 10;
416
+ }
417
+
418
+ .call-control-btn {
419
+ background: rgba(255,255,255,0.2);
420
  border: none;
421
  border-radius: 50%;
422
+ width: 70px;
423
+ height: 70px;
424
+ color: white;
425
  font-size: 24px;
426
  cursor: pointer;
427
+ backdrop-filter: blur(10px);
428
+ display: flex;
429
+ align-items: center;
430
+ justify-content: center;
431
  }
432
 
433
+ .end-call-btn {
434
+ background: #FF4444;
435
  }
436
 
437
+ .call-control-btn.active {
438
+ background: #4CAF50;
439
  }
440
 
441
+ /* Recording Overlay */
442
  .recording-overlay {
443
  position: fixed;
444
  top: 0;
 
469
  100% { transform: scale(0.8); opacity: 1; }
470
  }
471
 
472
+ /* Overlay */
473
+ .overlay {
474
+ position: fixed;
475
+ top: 0;
476
  left: 0;
477
+ width: 100%;
478
+ height: 100%;
479
+ background: rgba(0,0,0,0.5);
 
 
480
  display: none;
481
+ justify-content: center;
482
+ align-items: center;
483
+ z-index: 1500;
 
484
  }
485
 
486
+ .call-alert {
487
+ background: white;
488
+ padding: 25px;
489
+ border-radius: 15px;
490
+ text-align: center;
491
+ max-width: 300px;
492
+ width: 90%;
493
  }
494
 
495
+ .call-alert-buttons {
496
+ display: flex;
497
+ gap: 10px;
498
+ margin-top: 20px;
499
  }
500
 
501
+ .alert-btn {
502
+ flex: 1;
503
+ padding: 12px;
504
+ border: none;
505
+ border-radius: 8px;
506
+ font-weight: bold;
507
+ cursor: pointer;
508
  }
509
 
510
+ .accept-btn {
511
+ background: #4CAF50;
512
+ color: white;
 
 
 
513
  }
514
 
515
+ .reject-btn {
516
+ background: #FF4444;
517
+ color: white;
518
+ }
519
+
520
+ .no-video {
521
+ background: #333;
522
+ color: white;
523
+ display: flex;
524
+ align-items: center;
525
+ justify-content: center;
526
+ font-size: 16px;
527
  }
528
  </style>
529
  </head>
 
544
  <!-- Main Chat Screen -->
545
  <div id="chatScreen" class="app-container chat-screen">
546
  <div class="header">
547
+ <div class="header-info">
548
+ <h2>💬 Мессенджер</h2>
549
+ <div class="online-info">
550
+ <span id="onlineCount">0</span> пользователей онлайн
551
+ </div>
552
  </div>
553
+ <div class="call-buttons">
554
+ <button class="header-btn" onclick="toggleUsersSidebar()" title="Пользователи">👥</button>
555
  </div>
 
556
  </div>
557
 
558
  <div id="messagesContainer" class="messages-container">
 
561
  </div>
562
  </div>
563
 
 
 
 
 
 
564
  <div class="input-area">
565
+ <button class="action-btn" onclick="startVoiceRecording()" title="Голосовое сообщение">🎤</button>
566
  <input type="text" id="messageInput" class="message-input" placeholder="Введите сообщение..." maxlength="500">
567
  <button class="action-btn" onclick="sendMessage()" title="Отправить">📤</button>
568
  </div>
569
  </div>
570
 
571
+ <!-- Users Sidebar -->
572
+ <div id="usersSidebar" class="users-sidebar">
573
+ <div class="sidebar-header">
574
+ <h3>Пользователи онлайн</h3>
575
+ <button class="close-sidebar" onclick="toggleUsersSidebar()">✕</button>
576
+ </div>
577
+ <div id="usersList" class="users-list">
578
+ <!-- Users will be populated here -->
579
+ </div>
580
+ </div>
581
+
582
+ <!-- Call Screen -->
583
+ <div id="callScreen" class="call-screen">
584
+ <div class="video-container">
585
+ <div id="remoteVideoContainer" class="no-video remote-video">
586
+ <div>Нет видео</div>
587
+ </div>
588
+ <video id="localVideo" class="local-video" autoplay playsinline muted></video>
589
+ <div class="call-info">
590
+ <div class="call-status" id="callStatus">Звонок...</div>
591
+ <div class="call-timer" id="callTimer">00:00</div>
592
+ </div>
593
+ <div class="call-controls">
594
+ <button class="call-control-btn" id="muteBtn" onclick="toggleMute()">🎤</button>
595
+ <button class="call-control-btn" id="videoBtn" onclick="toggleVideo()">📹</button>
596
+ <button class="call-control-btn end-call-btn" onclick="endCall()">📞</button>
597
+ </div>
598
+ </div>
599
+ </div>
600
+
601
  <!-- Recording Overlay -->
602
  <div id="recordingOverlay" class="recording-overlay">
603
  <div class="recording-animation"></div>
 
605
  <p>Нажмите для остановки</p>
606
  </div>
607
 
608
+ <!-- Incoming Call Overlay -->
609
+ <div id="incomingCallOverlay" class="overlay">
610
+ <div class="call-alert">
611
+ <h3>Входящий звонок</h3>
612
+ <p id="callerName">...</p>
613
+ <div class="call-alert-buttons">
614
+ <button class="alert-btn reject-btn" onclick="rejectCall()">Отклонить</button>
615
+ <button class="alert-btn accept-btn" onclick="acceptCall()">Принять</button>
616
+ </div>
617
+ </div>
618
+ </div>
619
+
620
  <script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
621
  <script>
622
  const socket = io();
 
628
  let remoteStream = null;
629
  let peerConnection = null;
630
  let currentCall = null;
631
+ let callStartTime = null;
632
+ let callTimerInterval = null;
633
+ let isMuted = false;
634
+ let isVideoEnabled = true;
635
 
636
  // WebRTC configuration
637
  const rtcConfig = {
 
654
 
655
  socket.on('user_list_update', (data) => {
656
  document.getElementById('onlineCount').textContent = data.count;
657
+ updateUsersList(data.users || []);
 
658
  });
659
 
660
  socket.on('new_message', (message) => {
 
666
  });
667
 
668
  socket.on('incoming_call', (data) => {
669
+ showIncomingCall(data.from_user, data.call_id, data.isVideo);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
  });
671
 
672
  socket.on('call_answered', (data) => {
673
  if (data.answer) {
674
+ startCall(data.isVideo, false);
675
  } else {
676
+ alert('Пользователь отклонил звонок');
677
+ hideCallScreen();
678
  }
679
  });
680
 
681
+ socket.on('call_ended', () => {
682
+ alert('Звонок завершен');
683
+ endCall();
684
+ });
685
+
686
  socket.on('webrtc_offer', async (data) => {
687
  if (!peerConnection) {
688
  await createPeerConnection(false);
 
690
  await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
691
  const answer = await peerConnection.createAnswer();
692
  await peerConnection.setLocalDescription(answer);
693
+
694
  socket.emit('webrtc_answer', {
695
  answer: answer,
696
+ to_user: data.from_user,
697
+ call_id: data.call_id
698
  });
699
  });
700
 
 
747
  container.appendChild(messageDiv);
748
  container.scrollTop = container.scrollHeight;
749
 
750
+ // Remove placeholder if exists
751
  const placeholder = container.querySelector('div[style*="text-align: center"]');
752
  if (placeholder) {
753
  placeholder.remove();
 
775
  container.scrollTop = container.scrollHeight;
776
  }
777
 
778
+ function updateUsersList(users) {
779
+ const usersList = document.getElementById('usersList');
780
+ usersList.innerHTML = '';
781
+
782
+ const otherUsers = users.filter(user => user !== currentUser.username);
783
 
784
+ if (otherUsers.length === 0) {
785
+ usersList.innerHTML = '<div style="text-align: center; padding: 20px; color: #666;">Нет других пользователей онлайн</div>';
 
 
 
 
 
786
  return;
787
  }
788
 
789
+ otherUsers.forEach(user => {
790
+ const userItem = document.createElement('div');
791
+ userItem.className = 'user-item';
792
+ userItem.innerHTML = `
793
+ <div class="user-avatar">${user.charAt(0).toUpperCase()}</div>
794
+ <div class="user-info">
795
+ <div class="user-name">${user}</div>
796
+ <div class="user-status">online</div>
797
+ </div>
798
+ <button class="call-icon" onclick="startCallWithUser('${user}', false)" title="Аудиозвонок">📞</button>
799
+ <button class="call-icon" onclick="startCallWithUser('${user}', true)" title="Видеозвонок">📹</button>
800
+ `;
801
+ usersList.appendChild(userItem);
802
  });
803
  }
804
 
805
+ function toggleUsersSidebar() {
806
+ const sidebar = document.getElementById('usersSidebar');
807
+ sidebar.classList.toggle('open');
 
 
 
 
808
  }
809
 
810
  // Voice Messages
811
+ async function startVoiceRecording() {
812
+ if (isRecording) {
 
 
813
  stopRecording();
814
+ return;
815
  }
 
816
 
 
817
  try {
818
  const stream = await navigator.mediaDevices.getUserMedia({
819
  audio: {
 
857
  mediaRecorder.start();
858
  isRecording = true;
859
 
860
+ // Show recording overlay
861
  document.getElementById('recordingOverlay').style.display = 'flex';
862
 
863
  } catch (error) {
 
879
  audio.play().catch(e => console.error('Error playing audio:', e));
880
  }
881
 
882
+ // Call Functions
883
+ function startCallWithUser(targetUser, isVideo) {
884
+ currentCall = {
885
+ id: Date.now().toString(),
886
+ target: targetUser,
887
+ isVideo: isVideo,
888
+ isInitiator: true
889
+ };
890
+
891
+ socket.emit('call_request', {
892
+ call_id: currentCall.id,
893
+ from_user: currentUser.username,
894
+ to_user: targetUser,
895
+ isVideo: isVideo
896
+ });
897
+
898
+ showCallScreen(isVideo, `Звонок ${targetUser}...`);
899
+ }
 
 
 
 
 
 
 
 
 
 
900
 
901
+ function showIncomingCall(fromUser, callId, isVideo) {
902
+ currentCall = {
903
+ id: callId,
904
+ target: fromUser,
905
+ isVideo: isVideo,
906
+ isInitiator: false
907
+ };
908
+
909
+ document.getElementById('callerName').textContent = fromUser;
910
+ document.getElementById('incomingCallOverlay').style.display = 'flex';
911
  }
912
 
913
+ function acceptCall() {
914
+ document.getElementById('incomingCallOverlay').style.display = 'none';
915
+ socket.emit('call_answer', {
916
+ call_id: currentCall.id,
917
+ answer: true,
918
+ to_user: currentCall.target,
919
+ isVideo: currentCall.isVideo
920
+ });
921
+
922
+ startCall(currentCall.isVideo, true);
923
  }
924
 
925
+ function rejectCall() {
926
+ socket.emit('call_answer', {
927
+ call_id: currentCall.id,
928
+ answer: false,
929
+ to_user: currentCall.target
930
+ });
931
+
932
+ document.getElementById('incomingCallOverlay').style.display = 'none';
933
+ currentCall = null;
934
  }
935
 
936
+ async function startCall(isVideo, isInitiator) {
937
  try {
938
+ showCallScreen(isVideo, isInitiator ? 'Звонок...' : 'Разговор...');
939
 
940
  // Get local media stream
941
  const constraints = {
942
  audio: true,
943
  video: isVideo ? {
944
+ width: { ideal: 640 },
945
+ height: { ideal: 480 },
946
+ frameRate: { ideal: 24 }
947
  } : false
948
  };
949
 
950
  localStream = await navigator.mediaDevices.getUserMedia(constraints);
951
+ document.getElementById('localVideo').srcObject = localStream;
952
 
953
  // Create peer connection
954
+ await createPeerConnection(isInitiator);
955
 
956
+ if (isInitiator) {
957
  // Create and send offer
958
  const offer = await peerConnection.createOffer();
959
  await peerConnection.setLocalDescription(offer);
960
 
961
  socket.emit('webrtc_offer', {
962
  offer: offer,
963
+ to_user: currentCall.target,
964
+ call_id: currentCall.id
965
  });
966
+ }
967
+
968
+ // Start call timer
969
+ startCallTimer();
970
+
971
+ } catch (error) {
972
+ console.error('Error starting call:', error);
973
+ alert(`Ошибка начала звонка: ${error.message}`);
974
+ endCall();
975
+ }
976
+ }
977
+
978
+ async function createPeerConnection(isInitiator) {
979
+ peerConnection = new RTCPeerConnection(rtcConfig);
980
+
981
+ // Add local stream tracks
982
+ localStream.getTracks().forEach(track => {
983
+ peerConnection.addTrack(track, localStream);
984
+ });
985
+
986
+ // Handle incoming stream
987
+ peerConnection.ontrack = (event) => {
988
+ remoteStream = event.streams[0];
989
+ const remoteVideo = document.getElementById('remoteVideoContainer');
990
+
991
+ if (event.track.kind === 'video') {
992
+ // Create video element for remote stream
993
+ const videoElement = document.createElement('video');
994
+ videoElement.srcObject = remoteStream;
995
+ videoElement.autoplay = true;
996
+ videoElement.playsInline = true;
997
+ videoElement.className = 'remote-video';
998
+
999
+ remoteVideo.innerHTML = '';
1000
+ remoteVideo.appendChild(videoElement);
1001
+ } else if (event.track.kind === 'audio') {
1002
+ // For audio only, show message
1003
+ if (!document.querySelector('.remote-video video')) {
1004
+ remoteVideo.innerHTML = '<div>Аудиозвонок</div>';
1005
+ remoteVideo.className = 'no-video remote-video';
1006
+ }
1007
+ }
1008
+ };
1009
 
1010
+ // Handle ICE candidates
1011
+ peerConnection.onicecandidate = (event) => {
1012
+ if (event.candidate && currentCall) {
1013
+ socket.emit('webrtc_ice_candidate', {
1014
+ candidate: event.candidate,
1015
+ to_user: currentCall.target
1016
  });
1017
  }
1018
+ };
1019
 
1020
+ peerConnection.onconnectionstatechange = () => {
1021
+ console.log('Connection state:', peerConnection.connectionState);
1022
+ if (peerConnection.connectionState === 'connected') {
1023
+ document.getElementById('callStatus').textContent = 'Разговор';
1024
+ }
1025
+ };
1026
+ }
1027
 
1028
+ function showCallScreen(isVideo, status) {
1029
+ document.getElementById('callScreen').style.display = 'flex';
1030
+ document.getElementById('callStatus').textContent = status;
1031
+
1032
+ const remoteVideo = document.getElementById('remoteVideoContainer');
1033
+ if (!isVideo) {
1034
+ remoteVideo.innerHTML = '<div>Аудиозвонок</div>';
1035
+ remoteVideo.className = 'no-video remote-video';
1036
+ }
1037
+ }
1038
+
1039
+ function hideCallScreen() {
1040
+ document.getElementById('callScreen').style.display = 'none';
1041
+ document.getElementById('remoteVideoContainer').innerHTML = '<div>Нет видео</div>';
1042
+ document.getElementById('remoteVideoContainer').className = 'no-video remote-video';
1043
+
1044
+ if (callTimerInterval) {
1045
+ clearInterval(callTimerInterval);
1046
+ callTimerInterval = null;
1047
  }
1048
  }
1049
 
1050
+ function startCallTimer() {
1051
+ callStartTime = new Date();
1052
+ callTimerInterval = setInterval(() => {
1053
+ const now = new Date();
1054
+ const diff = Math.floor((now - callStartTime) / 1000);
1055
+ const minutes = Math.floor(diff / 60);
1056
+ const seconds = diff % 60;
1057
+ document.getElementById('callTimer').textContent =
1058
+ `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
1059
+ }, 1000);
1060
+ }
1061
+
1062
  function endCall() {
1063
  if (peerConnection) {
1064
  peerConnection.close();
1065
  peerConnection = null;
1066
  }
1067
+
1068
  if (localStream) {
1069
  localStream.getTracks().forEach(track => track.stop());
1070
  localStream = null;
1071
  }
1072
+
1073
+ if (currentCall) {
1074
+ socket.emit('call_end', {
1075
+ to_user: currentCall.target,
1076
+ call_id: currentCall.id
1077
+ });
1078
+ }
1079
+
1080
+ hideCallScreen();
1081
  currentCall = null;
1082
+ isMuted = false;
1083
+ isVideoEnabled = true;
1084
  }
1085
 
1086
+ function toggleMute() {
1087
+ if (localStream) {
1088
+ const audioTracks = localStream.getAudioTracks();
1089
+ if (audioTracks.length > 0) {
1090
+ isMuted = !isMuted;
1091
+ audioTracks[0].enabled = !isMuted;
1092
+ document.getElementById('muteBtn').classList.toggle('active', isMuted);
1093
+ document.getElementById('muteBtn').textContent = isMuted ? '🎤🚫' : '🎤';
1094
+ }
1095
+ }
1096
+ }
1097
 
1098
+ function toggleVideo() {
1099
+ if (localStream) {
1100
+ const videoTracks = localStream.getVideoTracks();
1101
+ if (videoTracks.length > 0) {
1102
+ isVideoEnabled = !isVideoEnabled;
1103
+ videoTracks[0].enabled = isVideoEnabled;
1104
+ document.getElementById('videoBtn').classList.toggle('active', !isVideoEnabled);
1105
+ document.getElementById('videoBtn').textContent = isVideoEnabled ? '📹' : '📹🚫';
1106
+
1107
+ // Hide/show local video
1108
+ document.getElementById('localVideo').style.display = isVideoEnabled ? 'block' : 'none';
1109
  }
1110
+ }
1111
  }
1112
 
1113
+ // Event listeners
1114
  document.getElementById('recordingOverlay').addEventListener('click', stopRecording);
1115
+
 
1116
  document.getElementById('messageInput').addEventListener('keypress', (e) => {
1117
  if (e.key === 'Enter') {
1118
  sendMessage();
 
1125
  }
1126
  });
1127
 
1128
+ // Close sidebar when clicking outside
1129
  document.addEventListener('click', (e) => {
1130
+ const sidebar = document.getElementById('usersSidebar');
1131
+ if (sidebar.classList.contains('open') &&
1132
+ !e.target.closest('.users-sidebar') &&
1133
+ !e.target.closest('.header-btn')) {
1134
+ sidebar.classList.remove('open');
1135
  }
1136
  });
 
 
 
 
 
1137
  </script>
1138
  </body>
1139
  </html>
 
1146
  @socketio.on('connect')
1147
  def handle_connect():
1148
  print(f'Client connected: {request.sid}')
 
1149
  emit('user_list_update', {
1150
  'count': len(users),
1151
  'users': [user_data['username'] for user_data in users.values()]
 
1155
  def handle_disconnect():
1156
  for user_id, user_data in list(users.items()):
1157
  if user_data.get('sid') == request.sid:
1158
+ # End any active calls
1159
+ for call_id, call_data in list(active_calls.items()):
1160
+ if call_data['from_user'] == user_data['username'] or call_data['to_user'] == user_data['username']:
1161
+ emit('call_ended', room=call_data['to_sid'])
1162
+ del active_calls[call_id]
1163
+
1164
  del users[user_id]
1165
  break
1166
 
 
1222
 
1223
  @socketio.on('call_request')
1224
  def handle_call_request(data):
1225
+ target_username = data.get('to_user')
1226
  target_sid = None
1227
 
1228
  for user_data in users.values():
1229
+ if user_data['username'] == target_username:
1230
  target_sid = user_data['sid']
1231
  break
1232
 
1233
  if target_sid:
1234
+ active_calls[data['call_id']] = {
1235
+ 'from_user': data['from_user'],
1236
+ 'to_user': target_username,
1237
+ 'from_sid': request.sid,
1238
+ 'to_sid': target_sid,
1239
+ 'is_video': data.get('isVideo', False)
1240
+ }
1241
+
1242
  emit('incoming_call', {
1243
  'call_id': data['call_id'],
1244
  'from_user': data['from_user'],
 
1247
 
1248
  @socketio.on('call_answer')
1249
  def handle_call_answer(data):
1250
+ call_id = data.get('call_id')
1251
+ if call_id in active_calls:
1252
+ call_data = active_calls[call_id]
1253
+
1254
+ if data['answer']:
1255
+ emit('call_answered', {
1256
+ 'answer': True,
1257
+ 'isVideo': call_data['is_video']
1258
+ }, room=call_data['from_sid'])
1259
+ else:
1260
+ emit('call_answered', {
1261
+ 'answer': False
1262
+ }, room=call_data['from_sid'])
1263
+ del active_calls[call_id]
1264
+
1265
+ @socketio.on('call_end')
1266
+ def handle_call_end(data):
1267
+ call_id = data.get('call_id')
1268
+ if call_id in active_calls:
1269
+ call_data = active_calls[call_id]
1270
+ emit('call_ended', room=call_data['to_sid'])
1271
+ del active_calls[call_id]
1272
 
1273
  @socketio.on('webrtc_offer')
1274
  def handle_webrtc_offer(data):
1275
+ target_username = data.get('to_user')
1276
  target_sid = None
1277
 
1278
  for user_data in users.values():
1279
+ if user_data['username'] == target_username:
1280
  target_sid = user_data['sid']
1281
  break
1282
 
1283
  if target_sid:
1284
  emit('webrtc_offer', {
1285
  'offer': data['offer'],
1286
+ 'from_user': data.get('from_user'),
1287
+ 'call_id': data.get('call_id')
1288
  }, room=target_sid)
1289
 
1290
  @socketio.on('webrtc_answer')
1291
  def handle_webrtc_answer(data):
1292
+ target_username = data.get('to_user')
1293
  target_sid = None
1294
 
1295
  for user_data in users.values():
1296
+ if user_data['username'] == target_username:
1297
  target_sid = user_data['sid']
1298
  break
1299
 
 
1305
 
1306
  @socketio.on('webrtc_ice_candidate')
1307
  def handle_webrtc_ice_candidate(data):
1308
+ target_username = data.get('to_user')
1309
  target_sid = None
1310
 
1311
  for user_data in users.values():
1312
+ if user_data['username'] == target_username:
1313
  target_sid = user_data['sid']
1314
  break
1315