Chat / main.py
Starchik1's picture
Update main.py
9c43c63 verified
import os
import uuid
import json
from datetime import datetime
from flask import Flask, request, jsonify, send_file
from flask_socketio import SocketIO, emit
import base64
import io
app = Flask(__name__)
app.config['SECRET_KEY'] = 'messenger-secret-key'
app.config['DEBUG'] = False
# Настройки для production среды
socketio = SocketIO(
app,
cors_allowed_origins="*",
ping_timeout=60,
ping_interval=25,
logger=True,
engineio_logger=False,
async_mode='eventlet',
max_http_buffer_size=100 * 1024 * 1024 # 100MB для больших файлов
)
# Хранилища данных
users = {}
messages = []
voice_messages = {}
active_calls = {}
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Мобильный Мессенджер</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f0f2f5;
height: 100vh;
overflow: hidden;
}
.app-container {
height: 100vh;
display: flex;
flex-direction: column;
max-width: 100%;
margin: 0 auto;
background: white;
}
/* Login Screen */
.login-screen {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.login-form {
background: rgba(255,255,255,0.1);
padding: 30px;
border-radius: 20px;
backdrop-filter: blur(10px);
width: 100%;
max-width: 320px;
}
.login-input {
width: 100%;
padding: 15px;
margin: 10px 0;
border: none;
border-radius: 25px;
font-size: 16px;
background: rgba(255,255,255,0.9);
text-align: center;
}
.login-btn {
width: 100%;
padding: 15px;
margin: 10px 0;
border: none;
border-radius: 25px;
background: #4CAF50;
color: white;
font-size: 16px;
font-weight: bold;
cursor: pointer;
}
/* Chat Screen */
.chat-screen {
display: none;
flex-direction: column;
height: 100%;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-info {
flex: 1;
}
.header h2 {
margin-bottom: 5px;
font-size: 18px;
}
.online-info {
font-size: 14px;
opacity: 0.9;
}
.call-buttons {
display: flex;
gap: 10px;
}
.header-btn {
background: rgba(255,255,255,0.2);
border: none;
border-radius: 50%;
width: 44px;
height: 44px;
color: white;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 10px;
background: #f8f9fa;
-webkit-overflow-scrolling: touch;
}
.message {
margin: 10px 0;
padding: 12px 15px;
border-radius: 18px;
max-width: 85%;
word-wrap: break-word;
position: relative;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message.own {
background: #007AFF;
color: white;
margin-left: auto;
border-bottom-right-radius: 5px;
}
.message.other {
background: white;
color: #333;
margin-right: auto;
border-bottom-left-radius: 5px;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.message-sender {
font-weight: bold;
font-size: 12px;
margin-bottom: 4px;
opacity: 0.8;
}
.message-time {
font-size: 11px;
opacity: 0.7;
margin-top: 5px;
text-align: right;
}
.voice-message {
display: flex;
align-items: center;
padding: 8px 0;
}
.voice-play-btn {
background: #007AFF;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
margin-right: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.voice-duration {
font-size: 14px;
color: #666;
}
.input-area {
display: flex;
padding: 10px;
background: white;
border-top: 1px solid #e0e0e0;
align-items: center;
}
.message-input {
flex: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 25px;
margin: 0 5px;
font-size: 16px;
outline: none;
background: #f8f9fa;
}
.action-btn {
background: none;
border: none;
font-size: 24px;
padding: 10px;
cursor: pointer;
border-radius: 50%;
transition: background 0.2s;
min-width: 44px;
min-height: 44px;
color: #007AFF;
}
.action-btn:active {
background: rgba(0,0,0,0.1);
}
/* Users List */
.users-sidebar {
position: fixed;
top: 0;
right: -300px;
width: 300px;
height: 100%;
background: white;
box-shadow: -2px 0 10px rgba(0,0,0,0.1);
transition: right 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
}
.users-sidebar.open {
right: 0;
}
.sidebar-header {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.close-sidebar {
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
}
.users-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.user-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.user-item:active {
background: #f5f5f5;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #007AFF;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 12px;
}
.user-info {
flex: 1;
}
.user-name {
font-weight: 500;
margin-bottom: 2px;
}
.user-status {
font-size: 12px;
color: #4CAF50;
}
.call-icon {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
padding: 8px;
color: #007AFF;
}
/* Call Screen */
.call-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #1a1a1a;
z-index: 2000;
display: none;
flex-direction: column;
}
.video-container {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.remote-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.local-video {
position: absolute;
bottom: 20px;
right: 20px;
width: 120px;
height: 160px;
border-radius: 10px;
border: 2px solid white;
object-fit: cover;
}
.call-info {
position: absolute;
top: 40px;
left: 0;
right: 0;
text-align: center;
color: white;
z-index: 10;
}
.call-status {
font-size: 18px;
margin-bottom: 5px;
}
.call-timer {
font-size: 24px;
font-weight: bold;
}
.call-controls {
position: absolute;
bottom: 40px;
left: 0;
right: 0;
display: flex;
justify-content: center;
gap: 20px;
z-index: 10;
}
.call-control-btn {
background: rgba(255,255,255,0.2);
border: none;
border-radius: 50%;
width: 70px;
height: 70px;
color: white;
font-size: 24px;
cursor: pointer;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
}
.end-call-btn {
background: #FF4444;
}
.call-control-btn.active {
background: #4CAF50;
}
/* Recording Overlay */
.recording-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
z-index: 1000;
color: white;
}
.recording-animation {
width: 100px;
height: 100px;
border: 3px solid white;
border-radius: 50%;
animation: pulse 1.5s infinite;
margin-bottom: 20px;
}
@keyframes pulse {
0% { transform: scale(0.8); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.7; }
100% { transform: scale(0.8); opacity: 1; }
}
/* Overlay */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 1500;
}
.call-alert {
background: white;
padding: 25px;
border-radius: 15px;
text-align: center;
max-width: 300px;
width: 90%;
}
.call-alert-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.alert-btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
}
.accept-btn {
background: #4CAF50;
color: white;
}
.reject-btn {
background: #FF4444;
color: white;
}
.no-video {
background: #333;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.connection-status {
position: fixed;
top: 10px;
right: 10px;
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
z-index: 3000;
}
.connected {
background: #4CAF50;
color: white;
}
.disconnected {
background: #FF4444;
color: white;
}
</style>
</head>
<body>
<!-- Connection Status -->
<div id="connectionStatus" class="connection-status disconnected" style="display: none;">
Подключение...
</div>
<!-- Login Screen -->
<div id="loginScreen" class="login-screen">
<div style="text-align: center; margin-bottom: 30px;">
<h1 style="font-size: 2.5em; margin-bottom: 10px;">💬</h1>
<h1>Мобильный Мессенджер</h1>
<p style="margin-top: 10px; opacity: 0.8;">Общение, звонки, видео</p>
</div>
<div class="login-form">
<input type="text" id="usernameInput" class="login-input" placeholder="Введите ваше имя" maxlength="20">
<button onclick="registerUser()" class="login-btn">Начать общение</button>
</div>
</div>
<!-- Main Chat Screen -->
<div id="chatScreen" class="app-container chat-screen">
<div class="header">
<div class="header-info">
<h2>💬 Мессенджер</h2>
<div class="online-info">
<span id="onlineCount">0</span> пользователей онлайн
</div>
</div>
<div class="call-buttons">
<button class="header-btn" onclick="toggleUsersSidebar()" title="Пользователи">👥</button>
</div>
</div>
<div id="messagesContainer" class="messages-container">
<div style="text-align: center; color: #666; padding: 20px;">
Начните общение!
</div>
</div>
<div class="input-area">
<button class="action-btn" onclick="startVoiceRecording()" title="Голосовое сообщение">🎤</button>
<input type="text" id="messageInput" class="message-input" placeholder="Введите сообщение..." maxlength="500">
<button class="action-btn" onclick="sendMessage()" title="Отправить">📤</button>
</div>
</div>
<!-- Users Sidebar -->
<div id="usersSidebar" class="users-sidebar">
<div class="sidebar-header">
<h3>Пользователи онлайн</h3>
<button class="close-sidebar" onclick="toggleUsersSidebar()">✕</button>
</div>
<div id="usersList" class="users-list">
<!-- Users will be populated here -->
</div>
</div>
<!-- Call Screen -->
<div id="callScreen" class="call-screen">
<div class="video-container">
<div id="remoteVideoContainer" class="no-video remote-video">
<div>Ожидание подключения...</div>
</div>
<video id="localVideo" class="local-video" autoplay playsinline muted></video>
<div class="call-info">
<div class="call-status" id="callStatus">Звонок...</div>
<div class="call-timer" id="callTimer">00:00</div>
</div>
<div class="call-controls">
<button class="call-control-btn" id="muteBtn" onclick="toggleMute()">🎤</button>
<button class="call-control-btn" id="videoBtn" onclick="toggleVideo()">📹</button>
<button class="call-control-btn end-call-btn" onclick="endCall()">📞</button>
</div>
</div>
</div>
<!-- Recording Overlay -->
<div id="recordingOverlay" class="recording-overlay">
<div class="recording-animation"></div>
<h3>Запись голосового сообщения...</h3>
<p>Нажмите для остановки</p>
</div>
<!-- Incoming Call Overlay -->
<div id="incomingCallOverlay" class="overlay">
<div class="call-alert">
<h3>Входящий звонок</h3>
<p id="callerName">...</p>
<div class="call-alert-buttons">
<button class="alert-btn reject-btn" onclick="rejectCall()">Отклонить</button>
<button class="alert-btn accept-btn" onclick="acceptCall()">Принять</button>
</div>
</div>
</div>
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
<script>
let socket;
let currentUser = null;
let mediaRecorder = null;
let audioChunks = [];
let isRecording = false;
let localStream = null;
let remoteStream = null;
let peerConnection = null;
let currentCall = null;
let callStartTime = null;
let callTimerInterval = null;
let isMuted = false;
let isVideoEnabled = true;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
// WebRTC configuration
const rtcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.anyfirewall.com:443?transport=tcp',
username: 'webrtc',
credential: 'webrtc'
}
]
};
function initializeSocket() {
// Получаем базовый URL для корректного подключения к WebSocket
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
socket = io({
transports: ['websocket', 'polling'],
upgrade: true,
rememberUpgrade: true,
timeout: 10000,
pingTimeout: 60000,
pingInterval: 25000,
reconnection: true,
reconnectionAttempts: MAX_RECONNECT_ATTEMPTS,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000
});
// Socket events
socket.on('connect', () => {
console.log('Connected to server');
updateConnectionStatus(true);
reconnectAttempts = 0;
// Re-register if we have a current user
if (currentUser) {
socket.emit('register', currentUser.username);
}
});
socket.on('disconnect', (reason) => {
console.log('Disconnected from server:', reason);
updateConnectionStatus(false);
if (reason === 'io server disconnect') {
// Server intentionally disconnected, try to reconnect
setTimeout(() => {
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
socket.connect();
reconnectAttempts++;
}
}, 1000);
}
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
updateConnectionStatus(false);
});
socket.on('registration_success', (data) => {
currentUser = data;
document.getElementById('loginScreen').style.display = 'none';
document.getElementById('chatScreen').style.display = 'flex';
updateConnectionStatus(true);
});
socket.on('user_list_update', (data) => {
document.getElementById('onlineCount').textContent = data.count;
updateUsersList(data.users || []);
});
socket.on('new_message', (message) => {
displayMessage(message);
});
socket.on('new_voice_message', (data) => {
displayVoiceMessage(data);
});
socket.on('incoming_call', (data) => {
showIncomingCall(data.from_user, data.call_id, data.isVideo);
});
socket.on('call_answered', (data) => {
if (data.answer) {
startCall(data.isVideo, false);
} else {
alert('Пользователь отклонил звонок');
hideCallScreen();
}
});
socket.on('call_ended', () => {
alert('Звонок завершен');
endCall();
});
socket.on('webrtc_offer', async (data) => {
console.log('Received WebRTC offer');
if (!peerConnection) {
await createPeerConnection(false);
}
try {
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
socket.emit('webrtc_answer', {
answer: answer,
to_user: data.from_user,
call_id: data.call_id
});
} catch (error) {
console.error('Error handling WebRTC offer:', error);
}
});
socket.on('webrtc_answer', async (data) => {
console.log('Received WebRTC answer');
if (peerConnection) {
try {
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer));
} catch (error) {
console.error('Error setting remote description:', error);
}
}
});
socket.on('webrtc_ice_candidate', async (data) => {
if (peerConnection && data.candidate) {
try {
await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
} catch (error) {
console.error('Error adding ICE candidate:', error);
}
}
});
}
function updateConnectionStatus(connected) {
const statusElement = document.getElementById('connectionStatus');
statusElement.style.display = 'block';
if (connected) {
statusElement.textContent = '✓ Подключено';
statusElement.className = 'connection-status connected';
} else {
statusElement.textContent = '✗ Отключено';
statusElement.className = 'connection-status disconnected';
}
}
// UI Functions
function registerUser() {
const username = document.getElementById('usernameInput').value.trim();
if (username) {
if (!socket || !socket.connected) {
initializeSocket();
}
socket.emit('register', username);
} else {
alert('Введите имя пользователя');
}
}
function sendMessage() {
const input = document.getElementById('messageInput');
const text = input.value.trim();
if (text && socket && socket.connected) {
socket.emit('message', {
sender: currentUser.username,
text: text
});
input.value = '';
} else if (!socket || !socket.connected) {
alert('Нет подключения к серверу');
}
}
function displayMessage(message) {
const container = document.getElementById('messagesContainer');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${message.sender === currentUser.username ? 'own' : 'other'}`;
messageDiv.innerHTML = `
<div class="message-sender">${message.sender}</div>
<div>${message.text}</div>
<div class="message-time">${new Date(message.timestamp).toLocaleTimeString()}</div>
`;
container.appendChild(messageDiv);
container.scrollTop = container.scrollHeight;
// Remove placeholder if exists
const placeholder = container.querySelector('div[style*="text-align: center"]');
if (placeholder) {
placeholder.remove();
}
}
function displayVoiceMessage(data) {
const container = document.getElementById('messagesContainer');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${data.sender === currentUser.username ? 'own' : 'other'}`;
messageDiv.innerHTML = `
<div class="message-sender">${data.sender}</div>
<div class="voice-message">
<button class="voice-play-btn" onclick="playVoiceMessage('${data.filename}')">▶</button>
<div>
<div>Голосовое сообщение</div>
<div class="voice-duration">${data.duration || '0'} сек</div>
</div>
</div>
<div class="message-time">${new Date(data.timestamp).toLocaleTimeString()}</div>
`;
container.appendChild(messageDiv);
container.scrollTop = container.scrollHeight;
}
function updateUsersList(users) {
const usersList = document.getElementById('usersList');
usersList.innerHTML = '';
const otherUsers = users.filter(user => user !== currentUser.username);
if (otherUsers.length === 0) {
usersList.innerHTML = '<div style="text-align: center; padding: 20px; color: #666;">Нет других пользователей онлайн</div>';
return;
}
otherUsers.forEach(user => {
const userItem = document.createElement('div');
userItem.className = 'user-item';
userItem.innerHTML = `
<div class="user-avatar">${user.charAt(0).toUpperCase()}</div>
<div class="user-info">
<div class="user-name">${user}</div>
<div class="user-status">online</div>
</div>
<button class="call-icon" onclick="startCallWithUser('${user}', false)" title="Аудиозвонок">📞</button>
<button class="call-icon" onclick="startCallWithUser('${user}', true)" title="Видеозвонок">📹</button>
`;
usersList.appendChild(userItem);
});
}
function toggleUsersSidebar() {
const sidebar = document.getElementById('usersSidebar');
sidebar.classList.toggle('open');
}
// Voice Messages
async function startVoiceRecording() {
if (isRecording) {
stopRecording();
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true
}
});
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
});
audioChunks = [];
let startTime = Date.now();
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
mediaRecorder.onstop = async () => {
const duration = Math.round((Date.now() - startTime) / 1000);
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
const reader = new FileReader();
reader.onload = function() {
const base64Audio = reader.result.split(',')[1];
if (socket && socket.connected) {
socket.emit('voice_message', {
audio_data: base64Audio,
sender: currentUser.username,
duration: duration
});
} else {
alert('Нет подключения для отправки голосового сообщения');
}
};
reader.readAsDataURL(audioBlob);
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
isRecording = true;
// Show recording overlay
document.getElementById('recordingOverlay').style.display = 'flex';
} catch (error) {
console.error('Error recording:', error);
alert('Ошибка доступа к микрофону');
}
}
function stopRecording() {
if (mediaRecorder && isRecording) {
mediaRecorder.stop();
isRecording = false;
document.getElementById('recordingOverlay').style.display = 'none';
}
}
function playVoiceMessage(filename) {
const audio = new Audio(`/voice/${filename}`);
audio.play().catch(e => console.error('Error playing audio:', e));
}
// Call Functions
function startCallWithUser(targetUser, isVideo) {
if (!socket || !socket.connected) {
alert('Нет подключения к серверу');
return;
}
currentCall = {
id: Date.now().toString(),
target: targetUser,
isVideo: isVideo,
isInitiator: true
};
socket.emit('call_request', {
call_id: currentCall.id,
from_user: currentUser.username,
to_user: targetUser,
isVideo: isVideo
});
showCallScreen(isVideo, `Звонок ${targetUser}...`);
}
function showIncomingCall(fromUser, callId, isVideo) {
currentCall = {
id: callId,
target: fromUser,
isVideo: isVideo,
isInitiator: false
};
document.getElementById('callerName').textContent = fromUser;
document.getElementById('incomingCallOverlay').style.display = 'flex';
}
function acceptCall() {
document.getElementById('incomingCallOverlay').style.display = 'none';
if (socket && socket.connected) {
socket.emit('call_answer', {
call_id: currentCall.id,
answer: true,
to_user: currentCall.target,
isVideo: currentCall.isVideo
});
startCall(currentCall.isVideo, true);
} else {
alert('Нет подключения к серверу');
}
}
function rejectCall() {
if (socket && socket.connected) {
socket.emit('call_answer', {
call_id: currentCall.id,
answer: false,
to_user: currentCall.target
});
}
document.getElementById('incomingCallOverlay').style.display = 'none';
currentCall = null;
}
async function startCall(isVideo, isInitiator) {
try {
showCallScreen(isVideo, isInitiator ? 'Звонок...' : 'Разговор...');
// Get local media stream
const constraints = {
audio: true,
video: isVideo ? {
width: { ideal: 640 },
height: { ideal: 480 },
frameRate: { ideal: 24 }
} : false
};
localStream = await navigator.mediaDevices.getUserMedia(constraints);
document.getElementById('localVideo').srcObject = localStream;
// Create peer connection
await createPeerConnection(isInitiator);
if (isInitiator) {
// Create and send offer
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
if (socket && socket.connected) {
socket.emit('webrtc_offer', {
offer: offer,
to_user: currentCall.target,
call_id: currentCall.id
});
}
}
// Start call timer
startCallTimer();
} catch (error) {
console.error('Error starting call:', error);
alert(`Ошибка начала звонка: ${error.message}`);
endCall();
}
}
async function createPeerConnection(isInitiator) {
peerConnection = new RTCPeerConnection(rtcConfig);
// Add local stream tracks
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// Handle incoming stream
peerConnection.ontrack = (event) => {
console.log('Received remote track:', event.track.kind);
remoteStream = event.streams[0];
const remoteVideo = document.getElementById('remoteVideoContainer');
if (event.track.kind === 'video') {
// Create video element for remote stream
const videoElement = document.createElement('video');
videoElement.srcObject = remoteStream;
videoElement.autoplay = true;
videoElement.playsInline = true;
videoElement.className = 'remote-video';
remoteVideo.innerHTML = '';
remoteVideo.appendChild(videoElement);
} else if (event.track.kind === 'audio') {
// For audio only, show message
if (!document.querySelector('.remote-video video')) {
remoteVideo.innerHTML = '<div>Аудиозвонок</div>';
remoteVideo.className = 'no-video remote-video';
}
}
};
// Handle ICE candidates
peerConnection.onicecandidate = (event) => {
if (event.candidate && currentCall && socket && socket.connected) {
socket.emit('webrtc_ice_candidate', {
candidate: event.candidate,
to_user: currentCall.target
});
}
};
peerConnection.onconnectionstatechange = () => {
console.log('Connection state:', peerConnection.connectionState);
if (peerConnection.connectionState === 'connected') {
document.getElementById('callStatus').textContent = 'Разговор';
}
};
peerConnection.oniceconnectionstatechange = () => {
console.log('ICE connection state:', peerConnection.iceConnectionState);
};
}
function showCallScreen(isVideo, status) {
document.getElementById('callScreen').style.display = 'flex';
document.getElementById('callStatus').textContent = status;
const remoteVideo = document.getElementById('remoteVideoContainer');
if (!isVideo) {
remoteVideo.innerHTML = '<div>Аудиозвонок</div>';
remoteVideo.className = 'no-video remote-video';
}
}
function hideCallScreen() {
document.getElementById('callScreen').style.display = 'none';
document.getElementById('remoteVideoContainer').innerHTML = '<div>Ожидание подключения...</div>';
document.getElementById('remoteVideoContainer').className = 'no-video remote-video';
if (callTimerInterval) {
clearInterval(callTimerInterval);
callTimerInterval = null;
}
}
function startCallTimer() {
callStartTime = new Date();
callTimerInterval = setInterval(() => {
const now = new Date();
const diff = Math.floor((now - callStartTime) / 1000);
const minutes = Math.floor(diff / 60);
const seconds = diff % 60;
document.getElementById('callTimer').textContent =
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}, 1000);
}
function endCall() {
if (peerConnection) {
peerConnection.close();
peerConnection = null;
}
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
if (currentCall && socket && socket.connected) {
socket.emit('call_end', {
to_user: currentCall.target,
call_id: currentCall.id
});
}
hideCallScreen();
currentCall = null;
isMuted = false;
isVideoEnabled = true;
}
function toggleMute() {
if (localStream) {
const audioTracks = localStream.getAudioTracks();
if (audioTracks.length > 0) {
isMuted = !isMuted;
audioTracks[0].enabled = !isMuted;
document.getElementById('muteBtn').classList.toggle('active', isMuted);
document.getElementById('muteBtn').textContent = isMuted ? '🎤🚫' : '🎤';
}
}
}
function toggleVideo() {
if (localStream) {
const videoTracks = localStream.getVideoTracks();
if (videoTracks.length > 0) {
isVideoEnabled = !isVideoEnabled;
videoTracks[0].enabled = isVideoEnabled;
document.getElementById('videoBtn').classList.toggle('active', !isVideoEnabled);
document.getElementById('videoBtn').textContent = isVideoEnabled ? '📹' : '📹🚫';
// Hide/show local video
document.getElementById('localVideo').style.display = isVideoEnabled ? 'block' : 'none';
}
}
}
// Event listeners
document.getElementById('recordingOverlay').addEventListener('click', stopRecording);
document.getElementById('messageInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
document.getElementById('usernameInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
registerUser();
}
});
// Close sidebar when clicking outside
document.addEventListener('click', (e) => {
const sidebar = document.getElementById('usersSidebar');
if (sidebar.classList.contains('open') &&
!e.target.closest('.users-sidebar') &&
!e.target.closest('.header-btn')) {
sidebar.classList.remove('open');
}
});
// Initialize socket on load
window.addEventListener('load', function() {
initializeSocket();
});
// Handle page visibility changes
document.addEventListener('visibilitychange', function() {
if (!document.hidden && (!socket || !socket.connected)) {
initializeSocket();
}
});
</script>
</body>
</html>
'''
@app.route('/')
def index():
return HTML_TEMPLATE
@socketio.on('connect')
def handle_connect():
print(f'Client connected: {request.sid}')
emit('user_list_update', {
'count': len(users),
'users': [user_data['username'] for user_data in users.values()]
})
@socketio.on('disconnect')
def handle_disconnect():
print(f'Client disconnected: {request.sid}')
# Find and remove the disconnected user
disconnected_user = None
for user_id, user_data in list(users.items()):
if user_data.get('sid') == request.sid:
disconnected_user = user_data['username']
# End any active calls
for call_id, call_data in list(active_calls.items()):
if call_data['from_user'] == disconnected_user or call_data['to_user'] == disconnected_user:
try:
emit('call_ended', room=call_data['to_sid'])
except:
pass
try:
emit('call_ended', room=call_data['from_sid'])
except:
pass
del active_calls[call_id]
del users[user_id]
break
if disconnected_user:
emit('user_list_update', {
'count': len(users),
'users': [user_data['username'] for user_data in users.values()]
}, broadcast=True)
@socketio.on('register')
def handle_register(username):
user_id = str(uuid.uuid4())
users[user_id] = {
'username': username,
'sid': request.sid,
'joined': datetime.now().isoformat()
}
emit('registration_success', {
'user_id': user_id,
'username': username
})
emit('user_list_update', {
'count': len(users),
'users': [user_data['username'] for user_data in users.values()]
}, broadcast=True)
@socketio.on('message')
def handle_message(data):
message = {
'id': str(uuid.uuid4()),
'sender': data['sender'],
'text': data['text'],
'timestamp': datetime.now().isoformat()
}
messages.append(message)
emit('new_message', message, broadcast=True)
@socketio.on('voice_message')
def handle_voice_message(data):
try:
audio_data = base64.b64decode(data['audio_data'])
filename = f"voice_{uuid.uuid4()}.webm"
with open(filename, 'wb') as f:
f.write(audio_data)
voice_data = {
'filename': filename,
'sender': data['sender'],
'duration': data.get('duration', 0),
'timestamp': datetime.now().isoformat()
}
voice_messages[filename] = voice_data
emit('new_voice_message', voice_data, broadcast=True)
except Exception as e:
print(f"Error handling voice message: {e}")
@socketio.on('call_request')
def handle_call_request(data):
target_username = data.get('to_user')
target_sid = None
for user_data in users.values():
if user_data['username'] == target_username:
target_sid = user_data['sid']
break
if target_sid:
active_calls[data['call_id']] = {
'from_user': data['from_user'],
'to_user': target_username,
'from_sid': request.sid,
'to_sid': target_sid,
'is_video': data.get('isVideo', False)
}
emit('incoming_call', {
'call_id': data['call_id'],
'from_user': data['from_user'],
'isVideo': data.get('isVideo', False)
}, room=target_sid)
@socketio.on('call_answer')
def handle_call_answer(data):
call_id = data.get('call_id')
if call_id in active_calls:
call_data = active_calls[call_id]
if data['answer']:
emit('call_answered', {
'answer': True,
'isVideo': call_data['is_video']
}, room=call_data['from_sid'])
else:
emit('call_answered', {
'answer': False
}, room=call_data['from_sid'])
del active_calls[call_id]
@socketio.on('call_end')
def handle_call_end(data):
call_id = data.get('call_id')
if call_id in active_calls:
call_data = active_calls[call_id]
try:
emit('call_ended', room=call_data['to_sid'])
except:
pass
try:
emit('call_ended', room=call_data['from_sid'])
except:
pass
del active_calls[call_id]
@socketio.on('webrtc_offer')
def handle_webrtc_offer(data):
target_username = data.get('to_user')
target_sid = None
for user_data in users.values():
if user_data['username'] == target_username:
target_sid = user_data['sid']
break
if target_sid:
emit('webrtc_offer', {
'offer': data['offer'],
'from_user': data.get('from_user'),
'call_id': data.get('call_id')
}, room=target_sid)
@socketio.on('webrtc_answer')
def handle_webrtc_answer(data):
target_username = data.get('to_user')
target_sid = None
for user_data in users.values():
if user_data['username'] == target_username:
target_sid = user_data['sid']
break
if target_sid:
emit('webrtc_answer', {
'answer': data['answer'],
'from_user': data.get('from_user')
}, room=target_sid)
@socketio.on('webrtc_ice_candidate')
def handle_webrtc_ice_candidate(data):
target_username = data.get('to_user')
target_sid = None
for user_data in users.values():
if user_data['username'] == target_username:
target_sid = user_data['sid']
break
if target_sid:
emit('webrtc_ice_candidate', {
'candidate': data['candidate'],
'from_user': data.get('from_user')
}, room=target_sid)
@app.route('/voice/<filename>')
def get_voice_message(filename):
if filename in voice_messages:
return send_file(filename, mimetype='audio/webm')
return 'File not found', 404
def cleanup_temp_files():
"""Очистка временных файлов"""
import glob
for filename in glob.glob("voice_*.webm"):
try:
os.remove(filename)
except:
pass
if __name__ == '__main__':
try:
print("Запуск мобильного мессенджера...")
print("Порт: 7860")
socketio.run(
app,
host='0.0.0.0',
port=7860,
debug=False,
allow_unsafe_werkzeug=True
)
finally:
cleanup_temp_files()