abdullahalioo commited on
Commit
b52fd0a
Β·
verified Β·
1 Parent(s): bf5766e

Upload 12 files

Browse files
Files changed (12) hide show
  1. .gitignore +30 -0
  2. .replit +36 -0
  3. README.md +244 -11
  4. app.py +379 -0
  5. data/messages.json +90 -0
  6. data/messages.json.lock +0 -0
  7. data/users.json +92 -0
  8. data/users.json.lock +0 -0
  9. replit.md +118 -0
  10. requirements.txt +7 -0
  11. templates/index.html +985 -0
  12. test_app.py +261 -0
.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ venv/
9
+ ENV/
10
+ .venv
11
+ *.egg-info/
12
+ dist/
13
+ build/
14
+
15
+ # File locks
16
+ *.lock
17
+
18
+ # IDE
19
+ .vscode/
20
+ .idea/
21
+ *.swp
22
+ *.swo
23
+ *~
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # Logs
30
+ *.log
.replit ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ modules = ["python-3.11"]
2
+ [agent]
3
+ expertMode = true
4
+ integrations = ["javascript_websocket:1.0.0"]
5
+
6
+ [workflows]
7
+ runButton = "Project"
8
+
9
+ [[workflows.workflow]]
10
+ name = "Project"
11
+ mode = "parallel"
12
+ author = "agent"
13
+
14
+ [[workflows.workflow.tasks]]
15
+ task = "workflow.run"
16
+ args = "run"
17
+
18
+ [[workflows.workflow]]
19
+ name = "run"
20
+ author = "agent"
21
+
22
+ [[workflows.workflow.tasks]]
23
+ task = "shell.exec"
24
+ args = "python app.py"
25
+ waitForPort = 5000
26
+
27
+ [workflows.workflow.metadata]
28
+ outputType = "webview"
29
+
30
+ [[ports]]
31
+ localPort = 5000
32
+ externalPort = 80
33
+
34
+ [[ports]]
35
+ localPort = 33559
36
+ externalPort = 3000
README.md CHANGED
@@ -1,11 +1,244 @@
1
- ---
2
- title: Whatsapp
3
- emoji: πŸ”₯
4
- colorFrom: pink
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # WhatsApp-like Web Chat Application
2
+
3
+ A real-time messaging web application built with Flask-SocketIO and vanilla JavaScript, featuring WebSocket-based instant messaging with persistent JSON storage.
4
+
5
+ ## Features
6
+
7
+ βœ… **User Registration & Authentication**
8
+ - Generate unique 8-digit user IDs
9
+ - Token-based authentication
10
+ - Session persistence with localStorage
11
+
12
+ βœ… **Real-time Messaging**
13
+ - WebSocket-powered instant message delivery
14
+ - Message history persistence in JSON files
15
+ - Thread-safe file operations with locking
16
+
17
+ βœ… **Message Status Lifecycle**
18
+ - βœ“ Sent (single check)
19
+ - βœ“βœ“ Delivered (double check, gray)
20
+ - βœ“βœ“ Read (double check, blue)
21
+
22
+ βœ… **Rich Chat Features**
23
+ - Typing indicators
24
+ - Online/offline presence
25
+ - Unread message badges
26
+ - Contact management (add by 8-digit ID)
27
+
28
+ βœ… **Responsive UI**
29
+ - WhatsApp-inspired design
30
+ - Dark theme
31
+ - Mobile-friendly layout
32
+
33
+ ## Project Structure
34
+
35
+ ```
36
+ .
37
+ β”œβ”€β”€ app.py # Flask-SocketIO backend server
38
+ β”œβ”€β”€ requirements.txt # Python dependencies
39
+ β”œβ”€β”€ data/
40
+ β”‚ β”œβ”€β”€ users.json # User data storage
41
+ β”‚ └── messages.json # Message history storage
42
+ β”œβ”€β”€ templates/
43
+ β”‚ └── index.html # Frontend application
44
+ └── test_app.py # Automated test script
45
+ ```
46
+
47
+ ## Data Formats
48
+
49
+ ### users.json
50
+ ```json
51
+ {
52
+ "12345678": {
53
+ "name": "Alice Demo",
54
+ "email": "alice@demo.com",
55
+ "token": "demo_token_alice_12345",
56
+ "contacts": ["87654321"],
57
+ "created_at": "2025-01-01T10:00:00"
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### messages.json
63
+ ```json
64
+ [
65
+ {
66
+ "message_id": "msg_demo_001",
67
+ "from": "12345678",
68
+ "to": "87654321",
69
+ "message": "Hey Bob! How are you?",
70
+ "timestamp": "2025-01-01T12:00:00",
71
+ "status": "read"
72
+ }
73
+ ]
74
+ ```
75
+
76
+ ## Running on Replit
77
+
78
+ 1. **The app is automatically configured to run on Replit**
79
+ - Just click the "Run" button
80
+ - The server will start on port 5000
81
+ - The webview will open automatically
82
+
83
+ 2. **Access the application**
84
+ - Click the webview pane to open the chat interface
85
+ - Or open the URL shown in the console
86
+
87
+ ## Manual Setup (Local Development)
88
+
89
+ 1. **Install dependencies**
90
+ ```bash
91
+ pip install -r requirements.txt
92
+ ```
93
+
94
+ 2. **Run the server**
95
+ ```bash
96
+ python app.py
97
+ ```
98
+
99
+ 3. **Access the application**
100
+ - Open your browser to `http://localhost:5000`
101
+
102
+ ## How to Use
103
+
104
+ ### 1. Register a New User
105
+ - Enter your name and email
106
+ - Click "Register"
107
+ - **Save your 8-digit User ID and Token** (displayed after registration)
108
+ - You'll be automatically logged in
109
+
110
+ ### 2. Login (Returning Users)
111
+ - Click "Login" link
112
+ - Enter your 8-digit User ID
113
+ - Enter your token
114
+ - Click "Login"
115
+
116
+ ### 3. Add Contacts
117
+ - In the chat interface, enter another user's 8-digit ID
118
+ - Click "Add" to add them to your contacts
119
+ - They will appear in your contacts list
120
+
121
+ ### 4. Start Chatting
122
+ - Click on a contact to open the chat
123
+ - Type your message and press Enter or click send
124
+ - See real-time message status updates
125
+ - Watch for typing indicators when your contact is typing
126
+
127
+ ### 5. Demo Users (Pre-configured)
128
+ You can login with these demo accounts:
129
+
130
+ **Alice Demo**
131
+ - User ID: `12345678`
132
+ - Token: `demo_token_alice_12345`
133
+
134
+ **Bob Demo**
135
+ - User ID: `87654321`
136
+ - Token: `demo_token_bob_67890`
137
+
138
+ ## Testing
139
+
140
+ The project includes an automated test script that validates all features.
141
+
142
+ **Note:** Test dependencies (`requests` and `python-socketio`) are included in `requirements.txt` and will be installed automatically.
143
+
144
+ Run the automated test script:
145
+
146
+ ```bash
147
+ python test_app.py
148
+ ```
149
+
150
+ This will test:
151
+ - User registration
152
+ - User login
153
+ - Adding contacts
154
+ - Sending messages
155
+ - Real-time WebSocket communication
156
+ - Message status updates (sent β†’ delivered β†’ read)
157
+ - Message persistence in JSON files
158
+
159
+ ## Technical Stack
160
+
161
+ ### Backend
162
+ - **Flask** - Web framework
163
+ - **Flask-SocketIO** - WebSocket support
164
+ - **eventlet** - Async networking library
165
+ - **filelock** - Thread-safe file operations
166
+
167
+ ### Frontend
168
+ - **Vanilla JavaScript** - No frameworks
169
+ - **Socket.IO Client** - WebSocket client
170
+ - **HTML5 & CSS3** - Responsive UI
171
+
172
+ ### Data Storage
173
+ - **JSON Files** - Persistent storage with file locking
174
+ - No database required
175
+
176
+ ## Security Notes
177
+
178
+ ⚠️ **This is a demonstration project with simplified security:**
179
+
180
+ - No password hashing (tokens are used instead)
181
+ - Tokens stored in localStorage (not secure for production)
182
+ - No HTTPS enforcement
183
+ - No rate limiting
184
+ - No input sanitization beyond basic HTML escaping
185
+
186
+ **For production use, implement:**
187
+ - Proper password hashing (bcrypt, argon2)
188
+ - Secure session management
189
+ - HTTPS/TLS encryption
190
+ - Input validation and sanitization
191
+ - Rate limiting and CSRF protection
192
+ - Database instead of JSON files
193
+
194
+ ## Features Implemented
195
+
196
+ - [x] User registration with 8-digit ID generation
197
+ - [x] Token-based login
198
+ - [x] Contact management (add by ID)
199
+ - [x] Real-time messaging via WebSockets
200
+ - [x] Message persistence in JSON files
201
+ - [x] Message status tracking (sent/delivered/read)
202
+ - [x] Typing indicators
203
+ - [x] Online/offline presence
204
+ - [x] Unread message badges
205
+ - [x] Thread-safe file operations
206
+ - [x] Responsive WhatsApp-like UI
207
+ - [x] Logout functionality
208
+
209
+ ## API Endpoints
210
+
211
+ ### REST API
212
+ - `POST /api/register` - Register new user
213
+ - `POST /api/login` - Login user
214
+ - `POST /api/contacts/add` - Add contact
215
+ - `POST /api/contacts` - Get user's contacts
216
+ - `POST /api/messages` - Get conversation messages
217
+
218
+ ### WebSocket Events
219
+ - `authenticate` - Authenticate WebSocket connection
220
+ - `send_message` - Send a message
221
+ - `typing` - Send typing indicator
222
+ - `mark_conversation_read` - Mark messages as read
223
+ - `message_delivered` - Update message delivery status
224
+ - `message_read` - Update message read status
225
+
226
+ ## Troubleshooting
227
+
228
+ **Messages not appearing in real-time?**
229
+ - Check browser console for WebSocket connection errors
230
+ - Ensure both users are logged in
231
+ - Refresh the page
232
+
233
+ **Can't add contact?**
234
+ - Verify the 8-digit ID is correct
235
+ - Make sure the user exists (registered)
236
+ - Can't add yourself as a contact
237
+
238
+ **Data not persisting?**
239
+ - Check that `data/` directory has write permissions
240
+ - Ensure no other process is locking the JSON files
241
+
242
+ ## License
243
+
244
+ This is a demonstration project for educational purposes.
app.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory
2
+ from flask_socketio import SocketIO, emit, join_room, leave_room
3
+ import json
4
+ import os
5
+ import random
6
+ import secrets
7
+ from datetime import datetime
8
+ from filelock import FileLock
9
+ from functools import wraps
10
+
11
+ app = Flask(__name__, static_folder='static', template_folder='templates')
12
+ app.config['SECRET_KEY'] = os.environ.get('SESSION_SECRET', secrets.token_hex(32))
13
+ socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet')
14
+
15
+ DATA_DIR = 'data'
16
+ USERS_FILE = os.path.join(DATA_DIR, 'users.json')
17
+ MESSAGES_FILE = os.path.join(DATA_DIR, 'messages.json')
18
+ USERS_LOCK = FileLock(f"{USERS_FILE}.lock")
19
+ MESSAGES_LOCK = FileLock(f"{MESSAGES_FILE}.lock")
20
+
21
+ active_users = {}
22
+
23
+ def init_data_files():
24
+ os.makedirs(DATA_DIR, exist_ok=True)
25
+
26
+ if not os.path.exists(USERS_FILE):
27
+ with USERS_LOCK:
28
+ with open(USERS_FILE, 'w') as f:
29
+ json.dump({}, f, indent=2)
30
+
31
+ if not os.path.exists(MESSAGES_FILE):
32
+ with MESSAGES_LOCK:
33
+ with open(MESSAGES_FILE, 'w') as f:
34
+ json.dump([], f, indent=2)
35
+
36
+ def read_json(file_path, lock):
37
+ with lock:
38
+ with open(file_path, 'r') as f:
39
+ return json.load(f)
40
+
41
+ def write_json(file_path, data, lock):
42
+ with lock:
43
+ with open(file_path, 'w') as f:
44
+ json.dump(data, f, indent=2)
45
+
46
+ def generate_user_id():
47
+ users = read_json(USERS_FILE, USERS_LOCK)
48
+ while True:
49
+ user_id = str(random.randint(10000000, 99999999))
50
+ if user_id not in users:
51
+ return user_id
52
+
53
+ def generate_token():
54
+ return secrets.token_urlsafe(32)
55
+
56
+ def generate_message_id():
57
+ return secrets.token_urlsafe(16)
58
+
59
+ def get_user_by_token(token):
60
+ users = read_json(USERS_FILE, USERS_LOCK)
61
+ for user_id, user_data in users.items():
62
+ if user_data.get('token') == token:
63
+ return user_id, user_data
64
+ return None, None
65
+
66
+ @app.route('/')
67
+ def index():
68
+ return send_from_directory('templates', 'index.html')
69
+
70
+ @app.route('/api/register', methods=['POST'])
71
+ def register():
72
+ data = request.json
73
+ name = data.get('name', '').strip()
74
+ email = data.get('email', '').strip()
75
+
76
+ if not name or not email:
77
+ return jsonify({'error': 'Name and email are required'}), 400
78
+
79
+ users = read_json(USERS_FILE, USERS_LOCK)
80
+
81
+ for user_data in users.values():
82
+ if user_data.get('email') == email:
83
+ return jsonify({'error': 'Email already registered'}), 400
84
+
85
+ user_id = generate_user_id()
86
+ token = generate_token()
87
+
88
+ users[user_id] = {
89
+ 'name': name,
90
+ 'email': email,
91
+ 'token': token,
92
+ 'contacts': [],
93
+ 'created_at': datetime.now().isoformat()
94
+ }
95
+
96
+ write_json(USERS_FILE, users, USERS_LOCK)
97
+
98
+ return jsonify({
99
+ 'user_id': user_id,
100
+ 'token': token,
101
+ 'name': name,
102
+ 'email': email
103
+ })
104
+
105
+ @app.route('/api/login', methods=['POST'])
106
+ def login():
107
+ data = request.json
108
+ user_id = data.get('user_id', '').strip()
109
+ token = data.get('token', '').strip()
110
+
111
+ if not user_id or not token:
112
+ return jsonify({'error': 'User ID and token are required'}), 400
113
+
114
+ users = read_json(USERS_FILE, USERS_LOCK)
115
+
116
+ if user_id not in users:
117
+ return jsonify({'error': 'Invalid user ID'}), 401
118
+
119
+ user_data = users[user_id]
120
+ if user_data.get('token') != token:
121
+ return jsonify({'error': 'Invalid token'}), 401
122
+
123
+ return jsonify({
124
+ 'user_id': user_id,
125
+ 'name': user_data['name'],
126
+ 'email': user_data['email'],
127
+ 'contacts': user_data.get('contacts', [])
128
+ })
129
+
130
+ @app.route('/api/contacts/add', methods=['POST'])
131
+ def add_contact():
132
+ data = request.json
133
+ token = data.get('token', '')
134
+ contact_id = data.get('contact_id', '').strip()
135
+
136
+ user_id, user_data = get_user_by_token(token)
137
+ if not user_id:
138
+ return jsonify({'error': 'Unauthorized'}), 401
139
+
140
+ if not contact_id:
141
+ return jsonify({'error': 'Contact ID is required'}), 400
142
+
143
+ users = read_json(USERS_FILE, USERS_LOCK)
144
+
145
+ if contact_id not in users:
146
+ return jsonify({'error': 'User not found'}), 404
147
+
148
+ if contact_id == user_id:
149
+ return jsonify({'error': 'Cannot add yourself as contact'}), 400
150
+
151
+ if contact_id in user_data.get('contacts', []):
152
+ return jsonify({'error': 'Contact already added'}), 400
153
+
154
+ users[user_id]['contacts'] = users[user_id].get('contacts', []) + [contact_id]
155
+ write_json(USERS_FILE, users, USERS_LOCK)
156
+
157
+ contact_data = users[contact_id]
158
+ return jsonify({
159
+ 'contact_id': contact_id,
160
+ 'name': contact_data['name'],
161
+ 'email': contact_data['email']
162
+ })
163
+
164
+ @app.route('/api/contacts', methods=['POST'])
165
+ def get_contacts():
166
+ data = request.json
167
+ token = data.get('token', '')
168
+
169
+ user_id, user_data = get_user_by_token(token)
170
+ if not user_id:
171
+ return jsonify({'error': 'Unauthorized'}), 401
172
+
173
+ users = read_json(USERS_FILE, USERS_LOCK)
174
+ contacts = []
175
+
176
+ for contact_id in user_data.get('contacts', []):
177
+ if contact_id in users:
178
+ contact = users[contact_id]
179
+ contacts.append({
180
+ 'user_id': contact_id,
181
+ 'name': contact['name'],
182
+ 'email': contact['email'],
183
+ 'online': contact_id in active_users
184
+ })
185
+
186
+ return jsonify({'contacts': contacts})
187
+
188
+ @app.route('/api/messages', methods=['POST'])
189
+ def get_messages():
190
+ data = request.json
191
+ token = data.get('token', '')
192
+ contact_id = data.get('contact_id', '')
193
+
194
+ user_id, user_data = get_user_by_token(token)
195
+ if not user_id:
196
+ return jsonify({'error': 'Unauthorized'}), 401
197
+
198
+ all_messages = read_json(MESSAGES_FILE, MESSAGES_LOCK)
199
+
200
+ conversation_messages = [
201
+ msg for msg in all_messages
202
+ if (msg['from'] == user_id and msg['to'] == contact_id) or
203
+ (msg['from'] == contact_id and msg['to'] == user_id)
204
+ ]
205
+
206
+ conversation_messages.sort(key=lambda x: x['timestamp'])
207
+
208
+ return jsonify({'messages': conversation_messages})
209
+
210
+ @socketio.on('connect')
211
+ def handle_connect():
212
+ print(f'Client connected: {request.sid}')
213
+
214
+ @socketio.on('authenticate')
215
+ def handle_authenticate(data):
216
+ token = data.get('token', '')
217
+ user_id, user_data = get_user_by_token(token)
218
+
219
+ if user_id:
220
+ active_users[user_id] = request.sid
221
+ join_room(user_id)
222
+
223
+ socketio.emit('user_status', {
224
+ 'user_id': user_id,
225
+ 'online': True
226
+ })
227
+
228
+ emit('authenticated', {'success': True, 'user_id': user_id})
229
+ print(f'User {user_id} ({user_data["name"]}) authenticated')
230
+ else:
231
+ emit('authenticated', {'success': False, 'error': 'Invalid token'})
232
+
233
+ @socketio.on('disconnect')
234
+ def handle_disconnect(reason=None):
235
+ user_id = None
236
+ for uid, sid in active_users.items():
237
+ if sid == request.sid:
238
+ user_id = uid
239
+ break
240
+
241
+ if user_id:
242
+ del active_users[user_id]
243
+ socketio.emit('user_status', {
244
+ 'user_id': user_id,
245
+ 'online': False
246
+ })
247
+ print(f'User {user_id} disconnected')
248
+
249
+ @socketio.on('send_message')
250
+ def handle_send_message(data):
251
+ token = data.get('token', '')
252
+ to_user_id = data.get('to', '')
253
+ message_text = data.get('message', '')
254
+
255
+ user_id, user_data = get_user_by_token(token)
256
+ if not user_id:
257
+ emit('error', {'message': 'Unauthorized'})
258
+ return
259
+
260
+ message_id = generate_message_id()
261
+ timestamp = datetime.now().isoformat()
262
+
263
+ message = {
264
+ 'message_id': message_id,
265
+ 'from': user_id,
266
+ 'to': to_user_id,
267
+ 'message': message_text,
268
+ 'timestamp': timestamp,
269
+ 'status': 'sent'
270
+ }
271
+
272
+ messages = read_json(MESSAGES_FILE, MESSAGES_LOCK)
273
+ messages.append(message)
274
+ write_json(MESSAGES_FILE, messages, MESSAGES_LOCK)
275
+
276
+ emit('message_sent', message)
277
+
278
+ if to_user_id in active_users:
279
+ message['status'] = 'delivered'
280
+ messages = read_json(MESSAGES_FILE, MESSAGES_LOCK)
281
+ for msg in messages:
282
+ if msg['message_id'] == message_id:
283
+ msg['status'] = 'delivered'
284
+ break
285
+ write_json(MESSAGES_FILE, messages, MESSAGES_LOCK)
286
+
287
+ socketio.emit('new_message', message, room=to_user_id)
288
+ emit('message_status_update', {
289
+ 'message_id': message_id,
290
+ 'status': 'delivered'
291
+ })
292
+
293
+ @socketio.on('message_delivered')
294
+ def handle_message_delivered(data):
295
+ message_id = data.get('message_id', '')
296
+
297
+ messages = read_json(MESSAGES_FILE, MESSAGES_LOCK)
298
+ for msg in messages:
299
+ if msg['message_id'] == message_id and msg['status'] == 'sent':
300
+ msg['status'] = 'delivered'
301
+ break
302
+ write_json(MESSAGES_FILE, messages, MESSAGES_LOCK)
303
+
304
+ for msg in messages:
305
+ if msg['message_id'] == message_id:
306
+ from_user_id = msg['from']
307
+ if from_user_id in active_users:
308
+ socketio.emit('message_status_update', {
309
+ 'message_id': message_id,
310
+ 'status': 'delivered'
311
+ }, room=from_user_id)
312
+ break
313
+
314
+ @socketio.on('message_read')
315
+ def handle_message_read(data):
316
+ message_id = data.get('message_id', '')
317
+
318
+ messages = read_json(MESSAGES_FILE, MESSAGES_LOCK)
319
+ for msg in messages:
320
+ if msg['message_id'] == message_id:
321
+ msg['status'] = 'read'
322
+ from_user_id = msg['from']
323
+ break
324
+ write_json(MESSAGES_FILE, messages, MESSAGES_LOCK)
325
+
326
+ if from_user_id in active_users:
327
+ socketio.emit('message_status_update', {
328
+ 'message_id': message_id,
329
+ 'status': 'read'
330
+ }, room=from_user_id)
331
+
332
+ @socketio.on('mark_conversation_read')
333
+ def handle_mark_conversation_read(data):
334
+ token = data.get('token', '')
335
+ contact_id = data.get('contact_id', '')
336
+
337
+ user_id, user_data = get_user_by_token(token)
338
+ if not user_id:
339
+ return
340
+
341
+ messages = read_json(MESSAGES_FILE, MESSAGES_LOCK)
342
+ updated_message_ids = []
343
+
344
+ for msg in messages:
345
+ if msg['from'] == contact_id and msg['to'] == user_id and msg['status'] != 'read':
346
+ msg['status'] = 'read'
347
+ updated_message_ids.append(msg['message_id'])
348
+
349
+ write_json(MESSAGES_FILE, messages, MESSAGES_LOCK)
350
+
351
+ if contact_id in active_users:
352
+ for msg_id in updated_message_ids:
353
+ socketio.emit('message_status_update', {
354
+ 'message_id': msg_id,
355
+ 'status': 'read'
356
+ }, room=contact_id)
357
+
358
+ @socketio.on('typing')
359
+ def handle_typing(data):
360
+ token = data.get('token', '')
361
+ to_user_id = data.get('to', '')
362
+ is_typing = data.get('typing', False)
363
+
364
+ user_id, user_data = get_user_by_token(token)
365
+ if not user_id:
366
+ return
367
+
368
+ if to_user_id in active_users:
369
+ socketio.emit('user_typing', {
370
+ 'user_id': user_id,
371
+ 'typing': is_typing
372
+ }, room=to_user_id)
373
+
374
+ # Hugging Face Spaces compatibility
375
+ if __name__ == '__main__':
376
+ init_data_files()
377
+ print("WhatsApp-like server starting on Hugging Face...")
378
+ print("Access the app at https://your-username-your-app-name.hf.space")
379
+ socketio.run(app, host='0.0.0.0', port=7860, debug=False)
data/messages.json ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "message_id": "msg_demo_001",
4
+ "from": "12345678",
5
+ "to": "87654321",
6
+ "message": "Hey Bob! How are you?",
7
+ "timestamp": "2025-01-01T12:00:00",
8
+ "status": "read"
9
+ },
10
+ {
11
+ "message_id": "msg_demo_002",
12
+ "from": "87654321",
13
+ "to": "12345678",
14
+ "message": "Hi Alice! I'm doing great, thanks!",
15
+ "timestamp": "2025-01-01T12:01:00",
16
+ "status": "read"
17
+ },
18
+ {
19
+ "message_id": "msg_demo_003",
20
+ "from": "12345678",
21
+ "to": "87654321",
22
+ "message": "Want to grab coffee later?",
23
+ "timestamp": "2025-01-01T12:02:00",
24
+ "status": "delivered"
25
+ },
26
+ {
27
+ "message_id": "u06O-1fCsAOtr_mfg-xw-Q",
28
+ "from": "13632423",
29
+ "to": "19871079",
30
+ "message": "hello",
31
+ "timestamp": "2025-11-03T10:26:44.253783",
32
+ "status": "read"
33
+ },
34
+ {
35
+ "message_id": "h2HYFDu-JherEGFujRayqA",
36
+ "from": "19871079",
37
+ "to": "13632423",
38
+ "message": "hello",
39
+ "timestamp": "2025-11-03T10:27:12.376759",
40
+ "status": "read"
41
+ },
42
+ {
43
+ "message_id": "i6SIp9RS_nXScmaPSar_4g",
44
+ "from": "19871079",
45
+ "to": "13632423",
46
+ "message": "who are you",
47
+ "timestamp": "2025-11-03T10:27:18.038752",
48
+ "status": "read"
49
+ },
50
+ {
51
+ "message_id": "ZGlv9Zg5JOGAVmogdLii-g",
52
+ "from": "13632423",
53
+ "to": "19871079",
54
+ "message": "i am fil\\ne",
55
+ "timestamp": "2025-11-03T10:27:24.204194",
56
+ "status": "read"
57
+ },
58
+ {
59
+ "message_id": "EOee-Nugk2aomXhsylODlg",
60
+ "from": "19871079",
61
+ "to": "13632423",
62
+ "message": "hello",
63
+ "timestamp": "2025-11-03T10:28:14.464749",
64
+ "status": "sent"
65
+ },
66
+ {
67
+ "message_id": "HlpNuvjFUb23XIgTjzfFqw",
68
+ "from": "13089813",
69
+ "to": "27739400",
70
+ "message": "Hello from automated test! \ud83d\udc4b",
71
+ "timestamp": "2025-11-03T10:29:42.199807",
72
+ "status": "read"
73
+ },
74
+ {
75
+ "message_id": "0l-NIELRxFVxvPNY_zQruQ",
76
+ "from": "19871079",
77
+ "to": "13632423",
78
+ "message": "hello",
79
+ "timestamp": "2025-11-03T10:29:57.738998",
80
+ "status": "sent"
81
+ },
82
+ {
83
+ "message_id": "8I36S7iw4T-IuB7NIxrZqg",
84
+ "from": "19871079",
85
+ "to": "13632423",
86
+ "message": "helloo",
87
+ "timestamp": "2025-11-03T10:30:15.790133",
88
+ "status": "sent"
89
+ }
90
+ ]
data/messages.json.lock ADDED
File without changes
data/users.json ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "12345678": {
3
+ "name": "Alice Demo",
4
+ "email": "alice@demo.com",
5
+ "token": "demo_token_alice_12345",
6
+ "contacts": [
7
+ "87654321"
8
+ ],
9
+ "created_at": "2025-01-01T10:00:00"
10
+ },
11
+ "87654321": {
12
+ "name": "Bob Demo",
13
+ "email": "bob@demo.com",
14
+ "token": "demo_token_bob_67890",
15
+ "contacts": [
16
+ "12345678"
17
+ ],
18
+ "created_at": "2025-01-01T11:00:00"
19
+ },
20
+ "13632423": {
21
+ "name": "abdullah ali",
22
+ "email": "abdullahali1692011@gmail.com",
23
+ "token": "qRp-NX-lIxtrzTLgvix7tPcekwcZpRmbNnQbZuhYHI0",
24
+ "contacts": [
25
+ "19871079"
26
+ ],
27
+ "created_at": "2025-11-03T10:25:58.261817"
28
+ },
29
+ "19871079": {
30
+ "name": "abdullah alioo",
31
+ "email": "abdullahtechhub1692011@gmail.com",
32
+ "token": "hSoljCRQ3kXPmmbJ4p5KvQyNgBmVCkd5cKOCj9A_ljA",
33
+ "contacts": [
34
+ "13632423"
35
+ ],
36
+ "created_at": "2025-11-03T10:26:26.208882"
37
+ },
38
+ "76582281": {
39
+ "name": "Test User 1",
40
+ "email": "testuser1_1762165678@test.com",
41
+ "token": "xG-xphaNlNaE-N5lbnIKNBd-8mIMzlQ24PtH6o0LESk",
42
+ "contacts": [
43
+ "12642270"
44
+ ],
45
+ "created_at": "2025-11-03T10:27:59.011170"
46
+ },
47
+ "12642270": {
48
+ "name": "Test User 2",
49
+ "email": "testuser2_1762165678@test.com",
50
+ "token": "jK7dDrMu2tDC-a4UJlt0vwhicSTyWSnIakwpuTBlTgo",
51
+ "contacts": [
52
+ "76582281"
53
+ ],
54
+ "created_at": "2025-11-03T10:27:59.018040"
55
+ },
56
+ "83708392": {
57
+ "name": "Test User 1",
58
+ "email": "testuser1_1762165705@test.com",
59
+ "token": "NMcpfP6HRJeRsSmsepU1c5TDQiwsnOyaLGwhFy18Zuw",
60
+ "contacts": [
61
+ "50185049"
62
+ ],
63
+ "created_at": "2025-11-03T10:28:25.930877"
64
+ },
65
+ "50185049": {
66
+ "name": "Test User 2",
67
+ "email": "testuser2_1762165705@test.com",
68
+ "token": "dZpu351F6YqLCDDZBIszWhIelfPqFOts3jtMQCtJAyE",
69
+ "contacts": [
70
+ "83708392"
71
+ ],
72
+ "created_at": "2025-11-03T10:28:25.937104"
73
+ },
74
+ "13089813": {
75
+ "name": "Test User 1",
76
+ "email": "testuser1_1762165778@test.com",
77
+ "token": "g0AZN0ngTzX2fk9THw_6k3jVG-KvXkjIVzEuselYi2c",
78
+ "contacts": [
79
+ "27739400"
80
+ ],
81
+ "created_at": "2025-11-03T10:29:38.088862"
82
+ },
83
+ "27739400": {
84
+ "name": "Test User 2",
85
+ "email": "testuser2_1762165778@test.com",
86
+ "token": "R0fICIcyQRqKn9WTgiR82v-kwUZ5PiILwnOhmtx-RPg",
87
+ "contacts": [
88
+ "13089813"
89
+ ],
90
+ "created_at": "2025-11-03T10:29:38.095038"
91
+ }
92
+ }
data/users.json.lock ADDED
File without changes
replit.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # WhatsApp-like Web Chat Application
2
+
3
+ ## Project Overview
4
+ A real-time messaging web application built with Flask-SocketIO (backend) and vanilla JavaScript (frontend). Uses WebSocket for instant messaging and JSON files for persistent data storage.
5
+
6
+ ## Purpose
7
+ Demonstration of a complete chat application with:
8
+ - User registration and authentication
9
+ - Real-time messaging
10
+ - Message status tracking (sent/delivered/read)
11
+ - Typing indicators and presence
12
+ - Persistent storage without a database
13
+
14
+ ## Current State
15
+ βœ… Fully functional and tested
16
+ βœ… All features implemented and working
17
+ βœ… Automated test suite passing
18
+ βœ… Server running on port 5000
19
+ βœ… Ready for use
20
+
21
+ ## Recent Changes (Nov 3, 2025)
22
+ - Initial implementation of complete chat system
23
+ - Fixed WebSocket broadcast parameter bug
24
+ - Fixed disconnect handler signature
25
+ - Added test dependencies (requests, python-socketio, websocket-client)
26
+ - All tests passing successfully
27
+
28
+ ## Project Architecture
29
+
30
+ ### Backend (app.py)
31
+ - **Flask** web framework with **Flask-SocketIO** for WebSocket support
32
+ - **eventlet** for async networking
33
+ - **filelock** for thread-safe JSON operations
34
+ - REST API endpoints for registration, login, contacts
35
+ - WebSocket handlers for real-time messaging, typing, presence
36
+
37
+ ### Frontend (templates/index.html)
38
+ - Vanilla JavaScript (no framework dependencies)
39
+ - Socket.IO client for WebSocket communication
40
+ - Responsive WhatsApp-inspired dark theme UI
41
+ - Real-time message updates and status tracking
42
+
43
+ ### Data Storage (data/)
44
+ - **users.json**: User profiles, tokens, contacts
45
+ - **messages.json**: All messages with status
46
+ - Thread-safe file locking ensures data integrity
47
+
48
+ ### Testing (test_app.py)
49
+ - Automated test suite covering all features
50
+ - Tests registration, login, contacts, messaging, status updates
51
+ - Validates message persistence
52
+
53
+ ## Key Features Implemented
54
+ 1. βœ… User registration with unique 8-digit ID generation
55
+ 2. βœ… Token-based authentication (simplified for demo)
56
+ 3. βœ… Contact management (add by ID)
57
+ 4. βœ… Real-time messaging via WebSockets
58
+ 5. βœ… Message status lifecycle: sent β†’ delivered β†’ read
59
+ 6. βœ… Typing indicators
60
+ 7. βœ… Online/offline presence tracking
61
+ 8. βœ… Unread message badges
62
+ 9. βœ… Message persistence in JSON files
63
+ 10. βœ… Responsive UI with chat list and conversation view
64
+
65
+ ## Security Notes (Demo Limitations)
66
+ ⚠️ This is a demonstration project with simplified security:
67
+ - No password hashing (tokens used instead)
68
+ - Tokens stored in localStorage
69
+ - No HTTPS enforcement
70
+ - No rate limiting
71
+ - Basic HTML escaping only
72
+
73
+ For production use, implement proper security measures.
74
+
75
+ ## How to Use
76
+ 1. Click "Run" button (server auto-starts on port 5000)
77
+ 2. Register a new user or login with demo accounts:
78
+ - Alice: ID `12345678`, Token `demo_token_alice_12345`
79
+ - Bob: ID `87654321`, Token `demo_token_bob_67890`
80
+ 3. Add contacts by entering their 8-digit ID
81
+ 4. Start chatting!
82
+
83
+ ## Testing
84
+ Run automated tests: `python test_app.py`
85
+
86
+ ## Dependencies
87
+ See `requirements.txt` for full list:
88
+ - Flask 3.0.0
89
+ - Flask-SocketIO 5.3.5
90
+ - eventlet 0.33.3
91
+ - filelock 3.13.1
92
+ - requests 2.31.0 (for tests)
93
+ - python-socketio 5.14.3 (for tests)
94
+ - websocket-client 1.6.4 (for tests)
95
+
96
+ ## File Structure
97
+ ```
98
+ β”œβ”€β”€ app.py # Backend server
99
+ β”œβ”€β”€ requirements.txt # Python dependencies
100
+ β”œβ”€β”€ README.md # User documentation
101
+ β”œβ”€β”€ test_app.py # Automated tests
102
+ β”œβ”€β”€ .gitignore # Git ignore patterns
103
+ β”œβ”€β”€ data/
104
+ β”‚ β”œβ”€β”€ users.json # User data
105
+ β”‚ └── messages.json # Message history
106
+ └── templates/
107
+ └── index.html # Frontend application
108
+ ```
109
+
110
+ ## Technical Notes
111
+ - Server runs on 0.0.0.0:5000 for Replit compatibility
112
+ - WebSocket uses Socket.IO v4 protocol
113
+ - JSON files use file locking for concurrent access safety
114
+ - Active users tracked in-memory for presence
115
+ - Debug mode enabled (disable for production)
116
+
117
+ ## User Preferences
118
+ None specified yet.
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Flask==3.0.0
2
+ Flask-SocketIO==5.3.5
3
+ eventlet==0.33.3
4
+ filelock==3.13.1
5
+ requests==2.31.0
6
+ python-socketio==5.14.3
7
+ websocket-client==1.6.4
templates/index.html ADDED
@@ -0,0 +1,985 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WhatsApp-like Chat</title>
7
+ <script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ background: #111b21;
18
+ height: 100vh;
19
+ overflow: hidden;
20
+ }
21
+
22
+ .auth-container {
23
+ display: flex;
24
+ justify-content: center;
25
+ align-items: center;
26
+ height: 100vh;
27
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
28
+ }
29
+
30
+ .auth-box {
31
+ background: white;
32
+ padding: 40px;
33
+ border-radius: 10px;
34
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
35
+ width: 400px;
36
+ max-width: 90%;
37
+ }
38
+
39
+ .auth-box h2 {
40
+ margin-bottom: 20px;
41
+ color: #333;
42
+ text-align: center;
43
+ }
44
+
45
+ .form-group {
46
+ margin-bottom: 20px;
47
+ }
48
+
49
+ .form-group label {
50
+ display: block;
51
+ margin-bottom: 5px;
52
+ color: #555;
53
+ font-weight: 500;
54
+ }
55
+
56
+ .form-group input {
57
+ width: 100%;
58
+ padding: 12px;
59
+ border: 2px solid #ddd;
60
+ border-radius: 5px;
61
+ font-size: 14px;
62
+ transition: border-color 0.3s;
63
+ }
64
+
65
+ .form-group input:focus {
66
+ outline: none;
67
+ border-color: #667eea;
68
+ }
69
+
70
+ .btn {
71
+ width: 100%;
72
+ padding: 12px;
73
+ background: #667eea;
74
+ color: white;
75
+ border: none;
76
+ border-radius: 5px;
77
+ font-size: 16px;
78
+ font-weight: 600;
79
+ cursor: pointer;
80
+ transition: background 0.3s;
81
+ }
82
+
83
+ .btn:hover {
84
+ background: #5568d3;
85
+ }
86
+
87
+ .toggle-auth {
88
+ text-align: center;
89
+ margin-top: 15px;
90
+ color: #666;
91
+ }
92
+
93
+ .toggle-auth a {
94
+ color: #667eea;
95
+ text-decoration: none;
96
+ font-weight: 600;
97
+ }
98
+
99
+ .error-msg {
100
+ background: #fee;
101
+ color: #c33;
102
+ padding: 10px;
103
+ border-radius: 5px;
104
+ margin-bottom: 15px;
105
+ font-size: 14px;
106
+ }
107
+
108
+ .success-msg {
109
+ background: #efe;
110
+ color: #3c3;
111
+ padding: 10px;
112
+ border-radius: 5px;
113
+ margin-bottom: 15px;
114
+ font-size: 14px;
115
+ }
116
+
117
+ .chat-container {
118
+ display: none;
119
+ height: 100vh;
120
+ background: #222e35;
121
+ }
122
+
123
+ .chat-layout {
124
+ display: flex;
125
+ height: 100%;
126
+ }
127
+
128
+ .sidebar {
129
+ width: 400px;
130
+ background: #111b21;
131
+ border-right: 1px solid #2a3942;
132
+ display: flex;
133
+ flex-direction: column;
134
+ }
135
+
136
+ .sidebar-header {
137
+ background: #202c33;
138
+ padding: 15px;
139
+ display: flex;
140
+ justify-content: space-between;
141
+ align-items: center;
142
+ }
143
+
144
+ .user-info {
145
+ color: #e9edef;
146
+ font-weight: 500;
147
+ }
148
+
149
+ .user-id-display {
150
+ font-size: 12px;
151
+ color: #8696a0;
152
+ }
153
+
154
+ .logout-btn {
155
+ background: #dc3545;
156
+ color: white;
157
+ border: none;
158
+ padding: 8px 16px;
159
+ border-radius: 5px;
160
+ cursor: pointer;
161
+ font-size: 14px;
162
+ }
163
+
164
+ .add-contact-section {
165
+ background: #202c33;
166
+ padding: 15px;
167
+ border-bottom: 1px solid #2a3942;
168
+ }
169
+
170
+ .add-contact-form {
171
+ display: flex;
172
+ gap: 10px;
173
+ }
174
+
175
+ .add-contact-input {
176
+ flex: 1;
177
+ padding: 10px;
178
+ border: 1px solid #2a3942;
179
+ border-radius: 5px;
180
+ background: #2a3942;
181
+ color: #e9edef;
182
+ font-size: 14px;
183
+ }
184
+
185
+ .add-contact-btn {
186
+ background: #00a884;
187
+ color: white;
188
+ border: none;
189
+ padding: 10px 20px;
190
+ border-radius: 5px;
191
+ cursor: pointer;
192
+ font-weight: 600;
193
+ }
194
+
195
+ .contacts-list {
196
+ flex: 1;
197
+ overflow-y: auto;
198
+ }
199
+
200
+ .contact-item {
201
+ padding: 15px;
202
+ border-bottom: 1px solid #2a3942;
203
+ cursor: pointer;
204
+ display: flex;
205
+ align-items: center;
206
+ transition: background 0.2s;
207
+ }
208
+
209
+ .contact-item:hover {
210
+ background: #202c33;
211
+ }
212
+
213
+ .contact-item.active {
214
+ background: #2a3942;
215
+ }
216
+
217
+ .contact-avatar {
218
+ width: 50px;
219
+ height: 50px;
220
+ border-radius: 50%;
221
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
222
+ display: flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ color: white;
226
+ font-weight: bold;
227
+ font-size: 20px;
228
+ margin-right: 15px;
229
+ }
230
+
231
+ .contact-info {
232
+ flex: 1;
233
+ }
234
+
235
+ .contact-name {
236
+ color: #e9edef;
237
+ font-weight: 500;
238
+ margin-bottom: 3px;
239
+ }
240
+
241
+ .contact-status {
242
+ font-size: 12px;
243
+ color: #8696a0;
244
+ }
245
+
246
+ .contact-status.online {
247
+ color: #00a884;
248
+ }
249
+
250
+ .unread-badge {
251
+ background: #00a884;
252
+ color: white;
253
+ border-radius: 50%;
254
+ width: 24px;
255
+ height: 24px;
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ font-size: 12px;
260
+ font-weight: bold;
261
+ }
262
+
263
+ .chat-area {
264
+ flex: 1;
265
+ display: flex;
266
+ flex-direction: column;
267
+ background: #0b141a;
268
+ }
269
+
270
+ .chat-header {
271
+ background: #202c33;
272
+ padding: 15px 20px;
273
+ display: flex;
274
+ align-items: center;
275
+ border-bottom: 1px solid #2a3942;
276
+ }
277
+
278
+ .chat-header-info {
279
+ flex: 1;
280
+ }
281
+
282
+ .chat-partner-name {
283
+ color: #e9edef;
284
+ font-weight: 500;
285
+ margin-bottom: 3px;
286
+ }
287
+
288
+ .chat-partner-status {
289
+ font-size: 13px;
290
+ color: #8696a0;
291
+ }
292
+
293
+ .typing-indicator {
294
+ font-size: 13px;
295
+ color: #00a884;
296
+ display: none;
297
+ }
298
+
299
+ .messages-container {
300
+ flex: 1;
301
+ overflow-y: auto;
302
+ padding: 20px;
303
+ background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcz48cGF0dGVybiBpZD0iYSIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjMGIxNDFhIi8+PHBhdGggZD0iTTUwIDUwbTAtMzVhMzUgMzUgMCAxIDAgNzAgMGEzNSAzNSAwIDEgMC03MCAwIiBzdHJva2U9IiMxMTFiMjEiIHN0cm9rZS13aWR0aD0iMC41IiBmaWxsPSJub25lIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+');
304
+ }
305
+
306
+ .message {
307
+ margin-bottom: 12px;
308
+ display: flex;
309
+ animation: messageSlideIn 0.3s ease;
310
+ }
311
+
312
+ @keyframes messageSlideIn {
313
+ from {
314
+ opacity: 0;
315
+ transform: translateY(10px);
316
+ }
317
+ to {
318
+ opacity: 1;
319
+ transform: translateY(0);
320
+ }
321
+ }
322
+
323
+ .message.sent {
324
+ justify-content: flex-end;
325
+ }
326
+
327
+ .message-bubble {
328
+ max-width: 60%;
329
+ padding: 8px 12px;
330
+ border-radius: 8px;
331
+ position: relative;
332
+ }
333
+
334
+ .message.received .message-bubble {
335
+ background: #202c33;
336
+ color: #e9edef;
337
+ }
338
+
339
+ .message.sent .message-bubble {
340
+ background: #005c4b;
341
+ color: #e9edef;
342
+ }
343
+
344
+ .message-text {
345
+ margin-bottom: 5px;
346
+ word-wrap: break-word;
347
+ }
348
+
349
+ .message-meta {
350
+ display: flex;
351
+ justify-content: flex-end;
352
+ align-items: center;
353
+ gap: 5px;
354
+ font-size: 11px;
355
+ color: #8696a0;
356
+ }
357
+
358
+ .message-status {
359
+ font-size: 16px;
360
+ }
361
+
362
+ .status-sent {
363
+ color: #8696a0;
364
+ }
365
+
366
+ .status-delivered {
367
+ color: #8696a0;
368
+ }
369
+
370
+ .status-read {
371
+ color: #53bdeb;
372
+ }
373
+
374
+ .message-input-area {
375
+ background: #202c33;
376
+ padding: 15px;
377
+ display: flex;
378
+ gap: 10px;
379
+ align-items: center;
380
+ }
381
+
382
+ .message-input {
383
+ flex: 1;
384
+ padding: 12px 15px;
385
+ border: none;
386
+ border-radius: 8px;
387
+ background: #2a3942;
388
+ color: #e9edef;
389
+ font-size: 15px;
390
+ resize: none;
391
+ max-height: 100px;
392
+ }
393
+
394
+ .message-input:focus {
395
+ outline: none;
396
+ }
397
+
398
+ .send-btn {
399
+ background: #00a884;
400
+ color: white;
401
+ border: none;
402
+ width: 50px;
403
+ height: 50px;
404
+ border-radius: 50%;
405
+ cursor: pointer;
406
+ font-size: 20px;
407
+ display: flex;
408
+ align-items: center;
409
+ justify-content: center;
410
+ transition: background 0.2s;
411
+ }
412
+
413
+ .send-btn:hover {
414
+ background: #06cf9c;
415
+ }
416
+
417
+ .empty-chat {
418
+ display: flex;
419
+ flex-direction: column;
420
+ align-items: center;
421
+ justify-content: center;
422
+ height: 100%;
423
+ color: #8696a0;
424
+ text-align: center;
425
+ padding: 40px;
426
+ }
427
+
428
+ .empty-chat-icon {
429
+ font-size: 80px;
430
+ margin-bottom: 20px;
431
+ opacity: 0.3;
432
+ }
433
+
434
+ .empty-chat-text {
435
+ font-size: 18px;
436
+ }
437
+
438
+ ::-webkit-scrollbar {
439
+ width: 6px;
440
+ }
441
+
442
+ ::-webkit-scrollbar-track {
443
+ background: #111b21;
444
+ }
445
+
446
+ ::-webkit-scrollbar-thumb {
447
+ background: #2a3942;
448
+ border-radius: 3px;
449
+ }
450
+
451
+ ::-webkit-scrollbar-thumb:hover {
452
+ background: #374955;
453
+ }
454
+
455
+ @media (max-width: 768px) {
456
+ .sidebar {
457
+ width: 100%;
458
+ }
459
+ .chat-area {
460
+ position: absolute;
461
+ top: 0;
462
+ left: 0;
463
+ right: 0;
464
+ bottom: 0;
465
+ display: none;
466
+ }
467
+ .chat-area.mobile-visible {
468
+ display: flex;
469
+ }
470
+ .sidebar.mobile-hidden {
471
+ display: none;
472
+ }
473
+ }
474
+ </style>
475
+ </head>
476
+ <body>
477
+ <div id="authContainer" class="auth-container">
478
+ <div class="auth-box">
479
+ <div id="registerForm">
480
+ <h2>Register</h2>
481
+ <div id="authMessage"></div>
482
+ <div class="form-group">
483
+ <label>Name</label>
484
+ <input type="text" id="registerName" placeholder="Enter your name">
485
+ </div>
486
+ <div class="form-group">
487
+ <label>Email</label>
488
+ <input type="email" id="registerEmail" placeholder="Enter your email">
489
+ </div>
490
+ <button class="btn" onclick="register()">Register</button>
491
+ <div class="toggle-auth">
492
+ Already have an account? <a href="#" onclick="toggleAuth('login'); return false;">Login</a>
493
+ </div>
494
+ </div>
495
+
496
+ <div id="loginForm" style="display: none;">
497
+ <h2>Login</h2>
498
+ <div id="loginMessage"></div>
499
+ <div class="form-group">
500
+ <label>User ID (8 digits)</label>
501
+ <input type="text" id="loginUserId" placeholder="Enter your 8-digit ID" maxlength="8">
502
+ </div>
503
+ <div class="form-group">
504
+ <label>Token</label>
505
+ <input type="text" id="loginToken" placeholder="Enter your token">
506
+ </div>
507
+ <button class="btn" onclick="login()">Login</button>
508
+ <div class="toggle-auth">
509
+ Don't have an account? <a href="#" onclick="toggleAuth('register'); return false;">Register</a>
510
+ </div>
511
+ </div>
512
+ </div>
513
+ </div>
514
+
515
+ <div id="chatContainer" class="chat-container">
516
+ <div class="chat-layout">
517
+ <div class="sidebar" id="sidebar">
518
+ <div class="sidebar-header">
519
+ <div>
520
+ <div class="user-info" id="currentUserName">User</div>
521
+ <div class="user-id-display">ID: <span id="currentUserId"></span></div>
522
+ </div>
523
+ <button class="logout-btn" onclick="logout()">Logout</button>
524
+ </div>
525
+ <div class="add-contact-section">
526
+ <div class="add-contact-form">
527
+ <input type="text" class="add-contact-input" id="addContactId"
528
+ placeholder="Enter 8-digit User ID" maxlength="8">
529
+ <button class="add-contact-btn" onclick="addContact()">Add</button>
530
+ </div>
531
+ </div>
532
+ <div class="contacts-list" id="contactsList"></div>
533
+ </div>
534
+
535
+ <div class="chat-area" id="chatArea">
536
+ <div class="empty-chat">
537
+ <div class="empty-chat-icon">πŸ’¬</div>
538
+ <div class="empty-chat-text">Select a contact to start chatting</div>
539
+ </div>
540
+ </div>
541
+ </div>
542
+ </div>
543
+
544
+ <script>
545
+ let socket;
546
+ let currentUser = null;
547
+ let currentChat = null;
548
+ let contacts = [];
549
+ let unreadCounts = {};
550
+ let typingTimeout = null;
551
+
552
+ function toggleAuth(mode) {
553
+ if (mode === 'login') {
554
+ document.getElementById('registerForm').style.display = 'none';
555
+ document.getElementById('loginForm').style.display = 'block';
556
+ document.getElementById('authMessage').innerHTML = '';
557
+ document.getElementById('loginMessage').innerHTML = '';
558
+ } else {
559
+ document.getElementById('registerForm').style.display = 'block';
560
+ document.getElementById('loginForm').style.display = 'none';
561
+ document.getElementById('authMessage').innerHTML = '';
562
+ document.getElementById('loginMessage').innerHTML = '';
563
+ }
564
+ }
565
+
566
+ async function register() {
567
+ const name = document.getElementById('registerName').value.trim();
568
+ const email = document.getElementById('registerEmail').value.trim();
569
+ const msgDiv = document.getElementById('authMessage');
570
+
571
+ if (!name || !email) {
572
+ msgDiv.innerHTML = '<div class="error-msg">Please fill all fields</div>';
573
+ return;
574
+ }
575
+
576
+ try {
577
+ const response = await fetch('/api/register', {
578
+ method: 'POST',
579
+ headers: { 'Content-Type': 'application/json' },
580
+ body: JSON.stringify({ name, email })
581
+ });
582
+
583
+ const data = await response.json();
584
+
585
+ if (response.ok) {
586
+ msgDiv.innerHTML = `<div class="success-msg">
587
+ Registration successful!<br>
588
+ <strong>Your ID: ${data.user_id}</strong><br>
589
+ <strong>Your Token: ${data.token}</strong><br>
590
+ <small>Save these credentials to login</small>
591
+ </div>`;
592
+ localStorage.setItem('user_id', data.user_id);
593
+ localStorage.setItem('token', data.token);
594
+
595
+ setTimeout(() => {
596
+ currentUser = data;
597
+ initChat();
598
+ }, 2000);
599
+ } else {
600
+ msgDiv.innerHTML = `<div class="error-msg">${data.error}</div>`;
601
+ }
602
+ } catch (error) {
603
+ msgDiv.innerHTML = `<div class="error-msg">Registration failed: ${error.message}</div>`;
604
+ }
605
+ }
606
+
607
+ async function login() {
608
+ const user_id = document.getElementById('loginUserId').value.trim();
609
+ const token = document.getElementById('loginToken').value.trim();
610
+ const msgDiv = document.getElementById('loginMessage');
611
+
612
+ if (!user_id || !token) {
613
+ msgDiv.innerHTML = '<div class="error-msg">Please fill all fields</div>';
614
+ return;
615
+ }
616
+
617
+ try {
618
+ const response = await fetch('/api/login', {
619
+ method: 'POST',
620
+ headers: { 'Content-Type': 'application/json' },
621
+ body: JSON.stringify({ user_id, token })
622
+ });
623
+
624
+ const data = await response.json();
625
+
626
+ if (response.ok) {
627
+ localStorage.setItem('user_id', user_id);
628
+ localStorage.setItem('token', token);
629
+ currentUser = { user_id, token, ...data };
630
+ initChat();
631
+ } else {
632
+ msgDiv.innerHTML = `<div class="error-msg">${data.error}</div>`;
633
+ }
634
+ } catch (error) {
635
+ msgDiv.innerHTML = `<div class="error-msg">Login failed: ${error.message}</div>`;
636
+ }
637
+ }
638
+
639
+ function initChat() {
640
+ document.getElementById('authContainer').style.display = 'none';
641
+ document.getElementById('chatContainer').style.display = 'block';
642
+ document.getElementById('currentUserName').textContent = currentUser.name;
643
+ document.getElementById('currentUserId').textContent = currentUser.user_id;
644
+
645
+ initWebSocket();
646
+ loadContacts();
647
+ }
648
+
649
+ function initWebSocket() {
650
+ socket = io();
651
+
652
+ socket.on('connect', () => {
653
+ console.log('Connected to server');
654
+ socket.emit('authenticate', { token: currentUser.token });
655
+ });
656
+
657
+ socket.on('authenticated', (data) => {
658
+ if (data.success) {
659
+ console.log('Authenticated successfully');
660
+ } else {
661
+ alert('Authentication failed: ' + data.error);
662
+ logout();
663
+ }
664
+ });
665
+
666
+ socket.on('user_status', (data) => {
667
+ updateContactStatus(data.user_id, data.online);
668
+ });
669
+
670
+ socket.on('new_message', (message) => {
671
+ if (currentChat && message.from === currentChat.user_id) {
672
+ appendMessage(message, false);
673
+ socket.emit('mark_conversation_read', {
674
+ token: currentUser.token,
675
+ contact_id: currentChat.user_id
676
+ });
677
+ } else {
678
+ unreadCounts[message.from] = (unreadCounts[message.from] || 0) + 1;
679
+ updateContactsList();
680
+ }
681
+ });
682
+
683
+ socket.on('message_sent', (message) => {
684
+ appendMessage(message, true);
685
+ });
686
+
687
+ socket.on('message_status_update', (data) => {
688
+ updateMessageStatus(data.message_id, data.status);
689
+ });
690
+
691
+ socket.on('user_typing', (data) => {
692
+ if (currentChat && data.user_id === currentChat.user_id) {
693
+ const typingIndicator = document.querySelector('.typing-indicator');
694
+ if (typingIndicator) {
695
+ typingIndicator.style.display = data.typing ? 'block' : 'none';
696
+ }
697
+ }
698
+ });
699
+ }
700
+
701
+ async function loadContacts() {
702
+ try {
703
+ const response = await fetch('/api/contacts', {
704
+ method: 'POST',
705
+ headers: { 'Content-Type': 'application/json' },
706
+ body: JSON.stringify({ token: currentUser.token })
707
+ });
708
+
709
+ const data = await response.json();
710
+ contacts = data.contacts || [];
711
+ updateContactsList();
712
+ } catch (error) {
713
+ console.error('Failed to load contacts:', error);
714
+ }
715
+ }
716
+
717
+ function updateContactsList() {
718
+ const list = document.getElementById('contactsList');
719
+ list.innerHTML = '';
720
+
721
+ contacts.forEach(contact => {
722
+ const item = document.createElement('div');
723
+ item.className = 'contact-item';
724
+ if (currentChat && currentChat.user_id === contact.user_id) {
725
+ item.className += ' active';
726
+ }
727
+
728
+ const initial = contact.name.charAt(0).toUpperCase();
729
+ const unreadCount = unreadCounts[contact.user_id] || 0;
730
+ const onlineStatus = contact.online ? 'online' : 'offline';
731
+
732
+ item.innerHTML = `
733
+ <div class="contact-avatar">${initial}</div>
734
+ <div class="contact-info">
735
+ <div class="contact-name">${contact.name}</div>
736
+ <div class="contact-status ${onlineStatus}">
737
+ ${contact.online ? 'Online' : 'Offline'}
738
+ </div>
739
+ </div>
740
+ ${unreadCount > 0 ? `<div class="unread-badge">${unreadCount}</div>` : ''}
741
+ `;
742
+
743
+ item.onclick = () => openChat(contact);
744
+ list.appendChild(item);
745
+ });
746
+ }
747
+
748
+ function updateContactStatus(userId, online) {
749
+ const contact = contacts.find(c => c.user_id === userId);
750
+ if (contact) {
751
+ contact.online = online;
752
+ updateContactsList();
753
+
754
+ if (currentChat && currentChat.user_id === userId) {
755
+ const statusEl = document.querySelector('.chat-partner-status');
756
+ if (statusEl && !statusEl.classList.contains('typing-indicator')) {
757
+ statusEl.textContent = online ? 'Online' : 'Offline';
758
+ }
759
+ }
760
+ }
761
+ }
762
+
763
+ async function addContact() {
764
+ const contactId = document.getElementById('addContactId').value.trim();
765
+
766
+ if (!contactId || contactId.length !== 8) {
767
+ alert('Please enter a valid 8-digit User ID');
768
+ return;
769
+ }
770
+
771
+ try {
772
+ const response = await fetch('/api/contacts/add', {
773
+ method: 'POST',
774
+ headers: { 'Content-Type': 'application/json' },
775
+ body: JSON.stringify({
776
+ token: currentUser.token,
777
+ contact_id: contactId
778
+ })
779
+ });
780
+
781
+ const data = await response.json();
782
+
783
+ if (response.ok) {
784
+ document.getElementById('addContactId').value = '';
785
+ loadContacts();
786
+ alert(`Contact added: ${data.name}`);
787
+ } else {
788
+ alert(data.error);
789
+ }
790
+ } catch (error) {
791
+ alert('Failed to add contact: ' + error.message);
792
+ }
793
+ }
794
+
795
+ async function openChat(contact) {
796
+ currentChat = contact;
797
+ unreadCounts[contact.user_id] = 0;
798
+ updateContactsList();
799
+
800
+ const chatArea = document.getElementById('chatArea');
801
+ chatArea.innerHTML = `
802
+ <div class="chat-header">
803
+ <div class="chat-header-info">
804
+ <div class="chat-partner-name">${contact.name}</div>
805
+ <div class="chat-partner-status">${contact.online ? 'Online' : 'Offline'}</div>
806
+ <div class="typing-indicator">typing...</div>
807
+ </div>
808
+ </div>
809
+ <div class="messages-container" id="messagesContainer"></div>
810
+ <div class="message-input-area">
811
+ <textarea class="message-input" id="messageInput"
812
+ placeholder="Type a message" rows="1"></textarea>
813
+ <button class="send-btn" onclick="sendMessage()">β–Ά</button>
814
+ </div>
815
+ `;
816
+
817
+ chatArea.classList.add('mobile-visible');
818
+ document.getElementById('sidebar').classList.add('mobile-hidden');
819
+
820
+ const messageInput = document.getElementById('messageInput');
821
+ messageInput.addEventListener('input', handleTyping);
822
+ messageInput.addEventListener('keypress', (e) => {
823
+ if (e.key === 'Enter' && !e.shiftKey) {
824
+ e.preventDefault();
825
+ sendMessage();
826
+ }
827
+ });
828
+
829
+ await loadMessages(contact.user_id);
830
+
831
+ socket.emit('mark_conversation_read', {
832
+ token: currentUser.token,
833
+ contact_id: contact.user_id
834
+ });
835
+ }
836
+
837
+ async function loadMessages(contactId) {
838
+ try {
839
+ const response = await fetch('/api/messages', {
840
+ method: 'POST',
841
+ headers: { 'Content-Type': 'application/json' },
842
+ body: JSON.stringify({
843
+ token: currentUser.token,
844
+ contact_id: contactId
845
+ })
846
+ });
847
+
848
+ const data = await response.json();
849
+ const messages = data.messages || [];
850
+
851
+ messages.forEach(msg => {
852
+ const isSent = msg.from === currentUser.user_id;
853
+ appendMessage(msg, isSent);
854
+ });
855
+
856
+ scrollToBottom();
857
+ } catch (error) {
858
+ console.error('Failed to load messages:', error);
859
+ }
860
+ }
861
+
862
+ function appendMessage(message, isSent) {
863
+ const container = document.getElementById('messagesContainer');
864
+ if (!container) return;
865
+
866
+ const msgDiv = document.createElement('div');
867
+ msgDiv.className = `message ${isSent ? 'sent' : 'received'}`;
868
+ msgDiv.dataset.messageId = message.message_id;
869
+
870
+ const time = new Date(message.timestamp).toLocaleTimeString('en-US', {
871
+ hour: '2-digit',
872
+ minute: '2-digit'
873
+ });
874
+
875
+ const statusIcon = getStatusIcon(message.status);
876
+
877
+ msgDiv.innerHTML = `
878
+ <div class="message-bubble">
879
+ <div class="message-text">${escapeHtml(message.message)}</div>
880
+ <div class="message-meta">
881
+ <span>${time}</span>
882
+ ${isSent ? `<span class="message-status">${statusIcon}</span>` : ''}
883
+ </div>
884
+ </div>
885
+ `;
886
+
887
+ container.appendChild(msgDiv);
888
+ scrollToBottom();
889
+ }
890
+
891
+ function getStatusIcon(status) {
892
+ if (status === 'sent') return '<span class="status-sent">βœ“</span>';
893
+ if (status === 'delivered') return '<span class="status-delivered">βœ“βœ“</span>';
894
+ if (status === 'read') return '<span class="status-read">βœ“βœ“</span>';
895
+ return '';
896
+ }
897
+
898
+ function updateMessageStatus(messageId, status) {
899
+ const msgDiv = document.querySelector(`[data-message-id="${messageId}"]`);
900
+ if (msgDiv) {
901
+ const statusSpan = msgDiv.querySelector('.message-status');
902
+ if (statusSpan) {
903
+ statusSpan.innerHTML = getStatusIcon(status);
904
+ }
905
+ }
906
+ }
907
+
908
+ function sendMessage() {
909
+ const input = document.getElementById('messageInput');
910
+ const message = input.value.trim();
911
+
912
+ if (!message || !currentChat) return;
913
+
914
+ socket.emit('send_message', {
915
+ token: currentUser.token,
916
+ to: currentChat.user_id,
917
+ message: message
918
+ });
919
+
920
+ input.value = '';
921
+ input.style.height = 'auto';
922
+
923
+ socket.emit('typing', {
924
+ token: currentUser.token,
925
+ to: currentChat.user_id,
926
+ typing: false
927
+ });
928
+ }
929
+
930
+ function handleTyping() {
931
+ if (!currentChat) return;
932
+
933
+ socket.emit('typing', {
934
+ token: currentUser.token,
935
+ to: currentChat.user_id,
936
+ typing: true
937
+ });
938
+
939
+ clearTimeout(typingTimeout);
940
+ typingTimeout = setTimeout(() => {
941
+ socket.emit('typing', {
942
+ token: currentUser.token,
943
+ to: currentChat.user_id,
944
+ typing: false
945
+ });
946
+ }, 1000);
947
+ }
948
+
949
+ function scrollToBottom() {
950
+ const container = document.getElementById('messagesContainer');
951
+ if (container) {
952
+ container.scrollTop = container.scrollHeight;
953
+ }
954
+ }
955
+
956
+ function escapeHtml(text) {
957
+ const map = {
958
+ '&': '&amp;',
959
+ '<': '&lt;',
960
+ '>': '&gt;',
961
+ '"': '&quot;',
962
+ "'": '&#039;'
963
+ };
964
+ return text.replace(/[&<>"']/g, m => map[m]);
965
+ }
966
+
967
+ function logout() {
968
+ localStorage.removeItem('user_id');
969
+ localStorage.removeItem('token');
970
+ if (socket) socket.disconnect();
971
+ location.reload();
972
+ }
973
+
974
+ window.onload = () => {
975
+ const savedUserId = localStorage.getItem('user_id');
976
+ const savedToken = localStorage.getItem('token');
977
+
978
+ if (savedUserId && savedToken) {
979
+ document.getElementById('loginUserId').value = savedUserId;
980
+ document.getElementById('loginToken').value = savedToken;
981
+ }
982
+ };
983
+ </script>
984
+ </body>
985
+ </html>
test_app.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for WhatsApp-like chat application
4
+ Demonstrates: Register β†’ Login β†’ Add Contact β†’ Send Message β†’ Status Updates
5
+ """
6
+
7
+ import requests
8
+ import time
9
+ import json
10
+ from socketio import SimpleClient
11
+
12
+ BASE_URL = 'http://localhost:5000'
13
+
14
+ def print_step(step, message):
15
+ print(f"\n{'='*60}")
16
+ print(f"STEP {step}: {message}")
17
+ print('='*60)
18
+
19
+ def test_registration():
20
+ print_step(1, "Testing User Registration")
21
+
22
+ user1_data = {
23
+ 'name': 'Test User 1',
24
+ 'email': f'testuser1_{int(time.time())}@test.com'
25
+ }
26
+
27
+ user2_data = {
28
+ 'name': 'Test User 2',
29
+ 'email': f'testuser2_{int(time.time())}@test.com'
30
+ }
31
+
32
+ print("\nπŸ“ Registering User 1...")
33
+ response1 = requests.post(f'{BASE_URL}/api/register', json=user1_data)
34
+ user1 = response1.json()
35
+ print(f"βœ… User 1 registered successfully!")
36
+ print(f" Name: {user1['name']}")
37
+ print(f" ID: {user1['user_id']}")
38
+ print(f" Token: {user1['token'][:20]}...")
39
+
40
+ print("\nπŸ“ Registering User 2...")
41
+ response2 = requests.post(f'{BASE_URL}/api/register', json=user2_data)
42
+ user2 = response2.json()
43
+ print(f"βœ… User 2 registered successfully!")
44
+ print(f" Name: {user2['name']}")
45
+ print(f" ID: {user2['user_id']}")
46
+ print(f" Token: {user2['token'][:20]}...")
47
+
48
+ return user1, user2
49
+
50
+ def test_login(user):
51
+ print_step(2, "Testing User Login")
52
+
53
+ login_data = {
54
+ 'user_id': user['user_id'],
55
+ 'token': user['token']
56
+ }
57
+
58
+ print(f"\nπŸ” Logging in as {user['name']}...")
59
+ response = requests.post(f'{BASE_URL}/api/login', json=login_data)
60
+ result = response.json()
61
+
62
+ if response.status_code == 200:
63
+ print(f"βœ… Login successful!")
64
+ print(f" User: {result['name']}")
65
+ print(f" Email: {result['email']}")
66
+ return True
67
+ else:
68
+ print(f"❌ Login failed: {result.get('error')}")
69
+ return False
70
+
71
+ def test_add_contact(user1, user2):
72
+ print_step(3, "Testing Add Contact")
73
+
74
+ contact_data = {
75
+ 'token': user1['token'],
76
+ 'contact_id': user2['user_id']
77
+ }
78
+
79
+ print(f"\nπŸ‘₯ {user1['name']} adding {user2['name']} as contact...")
80
+ response = requests.post(f'{BASE_URL}/api/contacts/add', json=contact_data)
81
+
82
+ if response.status_code == 200:
83
+ result = response.json()
84
+ print(f"βœ… Contact added successfully!")
85
+ print(f" Contact Name: {result['name']}")
86
+ print(f" Contact ID: {result['contact_id']}")
87
+ return True
88
+ else:
89
+ error = response.json().get('error')
90
+ print(f"❌ Failed to add contact: {error}")
91
+ return False
92
+
93
+ def test_get_contacts(user):
94
+ print_step(4, "Testing Get Contacts")
95
+
96
+ print(f"\nπŸ“‹ Getting contacts for {user['name']}...")
97
+ response = requests.post(f'{BASE_URL}/api/contacts',
98
+ json={'token': user['token']})
99
+
100
+ if response.status_code == 200:
101
+ result = response.json()
102
+ contacts = result['contacts']
103
+ print(f"βœ… Retrieved {len(contacts)} contact(s):")
104
+ for contact in contacts:
105
+ print(f" - {contact['name']} (ID: {contact['user_id']})")
106
+ return contacts
107
+ else:
108
+ print(f"❌ Failed to get contacts")
109
+ return []
110
+
111
+ def test_websocket_messaging(user1, user2):
112
+ print_step(5, "Testing WebSocket Real-time Messaging")
113
+
114
+ print("\nπŸ”Œ Connecting User 1 via WebSocket...")
115
+ client1 = SimpleClient()
116
+ client1.connect(BASE_URL)
117
+ client1.emit('authenticate', {'token': user1['token']})
118
+
119
+ event = client1.receive(timeout=5)
120
+ if event[0] == 'authenticated' and event[1]['success']:
121
+ print("βœ… User 1 authenticated via WebSocket")
122
+
123
+ print("\nπŸ”Œ Connecting User 2 via WebSocket...")
124
+ client2 = SimpleClient()
125
+ client2.connect(BASE_URL)
126
+ client2.emit('authenticate', {'token': user2['token']})
127
+
128
+ event = client2.receive(timeout=5)
129
+ if event[0] == 'authenticated' and event[1]['success']:
130
+ print("βœ… User 2 authenticated via WebSocket")
131
+
132
+ print("\nπŸ’¬ User 1 sending message to User 2...")
133
+ test_message = "Hello from automated test! πŸ‘‹"
134
+ client1.emit('send_message', {
135
+ 'token': user1['token'],
136
+ 'to': user2['user_id'],
137
+ 'message': test_message
138
+ })
139
+
140
+ time.sleep(1)
141
+
142
+ print("\nπŸ“¨ Checking for received messages...")
143
+ received_messages = []
144
+
145
+ try:
146
+ while True:
147
+ event = client2.receive(timeout=2)
148
+ print(f" Received event: {event[0]}")
149
+
150
+ if event[0] == 'new_message':
151
+ msg = event[1]
152
+ received_messages.append(msg)
153
+ print(f"βœ… Message received!")
154
+ print(f" From: {msg['from']}")
155
+ print(f" Message: {msg['message']}")
156
+ print(f" Status: {msg['status']}")
157
+
158
+ print(f"\nπŸ“– User 2 marking message as read...")
159
+ client2.emit('mark_conversation_read', {
160
+ 'token': user2['token'],
161
+ 'contact_id': user1['user_id']
162
+ })
163
+ break
164
+ except:
165
+ pass
166
+
167
+ time.sleep(1)
168
+
169
+ print("\nπŸ”„ Checking for status updates on User 1's side...")
170
+ try:
171
+ while True:
172
+ event = client1.receive(timeout=2)
173
+ if event[0] == 'message_status_update':
174
+ status_update = event[1]
175
+ print(f"βœ… Status update received!")
176
+ print(f" Message ID: {status_update['message_id']}")
177
+ print(f" New Status: {status_update['status']}")
178
+ if status_update['status'] == 'read':
179
+ print("\n✨ Message lifecycle complete: sent β†’ delivered β†’ read")
180
+ break
181
+ except:
182
+ pass
183
+
184
+ print("\nπŸ”Œ Disconnecting clients...")
185
+ client1.disconnect()
186
+ client2.disconnect()
187
+
188
+ return len(received_messages) > 0
189
+
190
+ def test_message_persistence(user1, user2):
191
+ print_step(6, "Testing Message Persistence")
192
+
193
+ print(f"\nπŸ“š Loading message history...")
194
+ response = requests.post(f'{BASE_URL}/api/messages',
195
+ json={
196
+ 'token': user1['token'],
197
+ 'contact_id': user2['user_id']
198
+ })
199
+
200
+ if response.status_code == 200:
201
+ result = response.json()
202
+ messages = result['messages']
203
+ print(f"βœ… Retrieved {len(messages)} message(s) from JSON storage:")
204
+ for msg in messages[-3:]:
205
+ print(f" [{msg['status']}] {msg['from'][:8]}... β†’ {msg['to'][:8]}...: {msg['message'][:30]}")
206
+ return True
207
+ else:
208
+ print(f"❌ Failed to load messages")
209
+ return False
210
+
211
+ def main():
212
+ print("\n" + "="*60)
213
+ print("WhatsApp-like Chat Application - Automated Test Suite")
214
+ print("="*60)
215
+
216
+ try:
217
+ user1, user2 = test_registration()
218
+ time.sleep(1)
219
+
220
+ test_login(user1)
221
+ time.sleep(1)
222
+
223
+ test_add_contact(user1, user2)
224
+ test_add_contact(user2, user1)
225
+ time.sleep(1)
226
+
227
+ test_get_contacts(user1)
228
+ time.sleep(1)
229
+
230
+ test_websocket_messaging(user1, user2)
231
+ time.sleep(1)
232
+
233
+ test_message_persistence(user1, user2)
234
+
235
+ print("\n" + "="*60)
236
+ print("βœ… ALL TESTS COMPLETED SUCCESSFULLY!")
237
+ print("="*60)
238
+ print("\nπŸ“‹ Test Summary:")
239
+ print(" βœ… User Registration")
240
+ print(" βœ… User Login")
241
+ print(" βœ… Add Contact")
242
+ print(" βœ… Get Contacts")
243
+ print(" βœ… Real-time Messaging via WebSocket")
244
+ print(" βœ… Message Status Lifecycle (sent β†’ delivered β†’ read)")
245
+ print(" βœ… Message Persistence in JSON")
246
+ print("\nπŸ’‘ You can now login with these test credentials:")
247
+ print(f" User 1 ID: {user1['user_id']}")
248
+ print(f" User 1 Token: {user1['token']}")
249
+ print(f" User 2 ID: {user2['user_id']}")
250
+ print(f" User 2 Token: {user2['token']}")
251
+
252
+ except Exception as e:
253
+ print(f"\n❌ Test failed with error: {str(e)}")
254
+ import traceback
255
+ traceback.print_exc()
256
+
257
+ if __name__ == '__main__':
258
+ print("\n⚠️ Make sure the server is running on http://localhost:5000")
259
+ print("Starting tests in 3 seconds...\n")
260
+ time.sleep(3)
261
+ main()