Aleksmorshen commited on
Commit
1d7daa1
·
verified ·
1 Parent(s): 1c1e1e0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +243 -231
app.py CHANGED
@@ -3,7 +3,7 @@ import os
3
  import datetime
4
  import uuid
5
  import werkzeug.utils
6
- import json
7
 
8
  app = Flask(__name__)
9
  app.config['UPLOAD_FOLDER'] = 'uploads_from_client'
@@ -15,7 +15,7 @@ os.makedirs(app.config['FILES_TO_CLIENT_FOLDER'], exist_ok=True)
15
  pending_command = None
16
  command_output = "Ожидание команд..."
17
  last_client_heartbeat = None
18
- current_client_path = "~"
19
  file_to_send_to_client = None
20
  device_status_info = {}
21
  notifications_history = []
@@ -59,6 +59,7 @@ HTML_TEMPLATE = """
59
  .hidden-section { display: none; }
60
  .status-item { margin-bottom: 10px; }
61
  .status-item strong { color: #333; }
 
62
  .notification-list { max-height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background-color: #fdfdfd;}
63
  .notification-item { border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px; }
64
  .notification-item:last-child { border-bottom: none; }
@@ -75,9 +76,9 @@ HTML_TEMPLATE = """
75
  document.querySelector(`.sidebar a[href="#${sectionId}"]`).classList.add('active');
76
  currentView = sectionId;
77
  if (sectionId === 'files') refreshClientPathDisplay();
78
- if (sectionId === 'device_status') requestDeviceStatus();
79
  if (sectionId === 'notifications') requestNotifications();
80
- refreshOutput(); // Refresh output specifically for the current view's needs
81
  }
82
 
83
  async function refreshOutput() {
@@ -85,18 +86,16 @@ HTML_TEMPLATE = """
85
  const response = await fetch('/get_status_output');
86
  const data = await response.json();
87
 
88
- // Update general output area if it's part of the current view logic
89
- const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
90
- if (data.output && generalOutputViews.includes(currentView)) {
91
  document.getElementById('outputArea').innerText = data.output;
92
  }
93
-
94
  if (data.last_heartbeat) {
95
  const statusDiv = document.getElementById('clientStatus');
96
  const lastBeat = new Date(data.last_heartbeat);
97
  const now = new Date();
98
  const diffSeconds = (now - lastBeat) / 1000;
99
- if (diffSeconds < 45) {
100
  statusDiv.className = 'status online';
101
  statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
102
  } else {
@@ -108,33 +107,33 @@ HTML_TEMPLATE = """
108
  document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН';
109
  }
110
 
111
- if (currentView === 'files') {
112
- if(data.current_path) document.getElementById('currentPathDisplay').innerText = data.current_path;
113
- if (data.output && data.output.startsWith("Содержимое")) {
114
  renderFileList(data.output, data.current_path);
115
- } else if (data.output) { // If not a listing, show raw output in file list area for context
116
  document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`;
117
  }
118
  }
119
-
120
  const filesResponse = await fetch('/list_uploaded_files');
121
  const filesData = await filesResponse.json();
122
  const serverFileListUl = document.getElementById('serverUploadedFiles');
123
- serverFileListUl.innerHTML = '';
124
  if (filesData.files && filesData.files.length > 0) {
125
  filesData.files.forEach(file => {
126
  const li = document.createElement('li');
127
  const a = document.createElement('a');
128
  a.href = '/uploads_from_client/' + encodeURIComponent(file);
129
  a.textContent = file;
130
- a.target = '_blank';
131
  li.appendChild(a);
132
  serverFileListUl.appendChild(li);
133
  });
134
  } else {
135
  serverFileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
136
  }
137
-
138
  if (data.device_status && currentView === 'device_status') {
139
  updateDeviceStatusDisplay(data.device_status);
140
  }
@@ -144,40 +143,52 @@ HTML_TEMPLATE = """
144
 
145
  } catch (error) {
146
  console.error("Error refreshing data:", error);
147
- const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
148
- if (generalOutputViews.includes(currentView)) {
149
  document.getElementById('outputArea').innerText = "Ошибка обновления данных с сервера.";
150
  }
151
  }
152
  }
153
-
154
  function updateDeviceStatusDisplay(status) {
155
- document.getElementById('batteryStatus').innerHTML = status.battery ? `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})` : '<strong>Заряд:</strong> Н/Д';
156
- document.getElementById('locationStatus').innerHTML = (status.location && status.location.latitude !== undefined) ? `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)` : '<strong>Локация:</strong> Н/Д (Запросите для обновления)';
157
- if (status.location && status.location.latitude && status.location.longitude) {
158
- document.getElementById('locationMapLink').innerHTML = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`;
159
- } else {
160
- document.getElementById('locationMapLink').innerHTML = '';
161
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  document.getElementById('processesStatus').innerHTML = status.processes ? `<pre>${status.processes}</pre>` : '<strong>Процессы:</strong> Н/Д (Запросите для обновления)';
 
 
 
 
 
 
163
  }
164
 
165
  function renderFileList(lsOutput, currentPath) {
166
  const fileListUl = document.getElementById('fileList');
167
  fileListUl.innerHTML = '';
168
- const lines = lsOutput.split('\\n'); // Assuming client sends \n, but Flask might re-escape. Check actual data if issues.
169
- // If outputArea shows literal \n, then split by '\n' might be needed.
170
- // For now, assuming client replaces \n with \\n for JSON string.
171
-
172
- let isRootOrHome = (currentPath === '/' || currentPath === '~'); // Simplified check
173
- // Check for path like /data/data/com.termux/files/home too, which is effectively ~
174
- if (currentPath.startsWith('/data/data/com.termux/files/home') &&
175
- (currentPath.length === '/data/data/com.termux/files/home'.length || currentPath.length === '/data/data/com.termux/files/home'.length + 1 && currentPath.endsWith('/'))) {
176
- isRootOrHome = true;
177
- }
178
-
179
 
180
- if (!isRootOrHome) {
 
 
 
181
  const parentLi = document.createElement('li');
182
  parentLi.className = 'dir';
183
  const parentA = document.createElement('a');
@@ -190,46 +201,40 @@ HTML_TEMPLATE = """
190
 
191
  lines.forEach(line => {
192
  if (line.trim() === '' || line.startsWith("Содержимое")) return;
193
- const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/); // Matches literal [D] or [F]
194
- if (!parts) { // Fallback for lines not matching [D]/[F] format, e.g. error messages
195
- const li = document.createElement('li');
196
- li.textContent = line;
197
- fileListUl.appendChild(li);
198
- return;
199
- };
200
-
201
- const type = parts[1];
202
  const name = parts[2].trim();
203
-
204
  const li = document.createElement('li');
205
  const nameSpan = document.createElement('span');
206
  const a = document.createElement('a');
207
  a.href = '#';
208
-
209
  if (type === '[D]') {
210
  li.className = 'dir';
211
  a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
212
  a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
213
  nameSpan.appendChild(a);
214
- } else { // Assuming [F]
215
  li.className = 'file';
216
  a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
217
- // Make file name clickable for future actions if needed, for now no-op
218
- a.onclick = (e) => { e.preventDefault(); /* Potentially show file info */ };
219
  nameSpan.appendChild(a);
220
 
221
  const downloadBtn = document.createElement('button');
222
  downloadBtn.className = 'download-btn';
223
  downloadBtn.textContent = 'Скачать';
224
  downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
225
- li.appendChild(nameSpan);
226
- li.appendChild(downloadBtn);
227
  }
228
- if (type === '[D]') li.appendChild(nameSpan);
229
  fileListUl.appendChild(li);
230
  });
231
  }
232
-
233
  function renderNotifications(notifications) {
234
  const notificationListDiv = document.getElementById('notificationList');
235
  notificationListDiv.innerHTML = '';
@@ -237,14 +242,12 @@ HTML_TEMPLATE = """
237
  notifications.forEach(n => {
238
  const itemDiv = document.createElement('div');
239
  itemDiv.className = 'notification-item';
240
- const title = n.title || (n.ticker && n.ticker !== "null" ? n.ticker : 'Без заголовка'); // Better title fallback
241
- const content = n.content || (n.text && n.text !== "null" ? n.text : 'Нет содержимого'); // Better content fallback
242
  itemDiv.innerHTML = `
243
- <strong>${title} (ID: ${n.id || 'N/A'})</strong>
244
  <span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
245
  <span><strong>Тег:</strong> ${n.tag || 'N/A'}, <strong>Ключ:</strong> ${n.key || 'N/A'}</span>
246
  <span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span>
247
- <p>${content}</p>
248
  `;
249
  notificationListDiv.appendChild(itemDiv);
250
  });
@@ -252,15 +255,24 @@ HTML_TEMPLATE = """
252
  notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
253
  }
254
  }
255
-
 
 
 
 
 
 
 
 
 
 
256
  async function sendGenericCommand(payload) {
257
  try {
258
- // Show "Sending command..." in the main output area if it's relevant to the current view.
259
- const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
260
- if (generalOutputViews.includes(currentView)) {
 
261
  document.getElementById('outputArea').innerText = "Отправка команды...";
262
- } else if (currentView === 'files') {
263
- // For file view, commands might update file list, don't overwrite general output unless error
264
  }
265
 
266
  const response = await fetch('/send_command', {
@@ -270,35 +282,29 @@ HTML_TEMPLATE = """
270
  });
271
  if (!response.ok) {
272
  console.error("Server error sending command");
273
- if (generalOutputViews.includes(currentView)) {
274
- document.getElementById('outputArea').innerText = "Ошибка сервера при отправке команды.";
275
- }
276
- }
277
- // Result will be shown by refreshOutput
278
  } catch (error) {
279
  console.error("Network error sending command:", error);
280
- if (generalOutputViews.includes(currentView)) {
281
- document.getElementById('outputArea').innerText = "Сетевая ошибка при отправке команды.";
282
- }
283
  }
284
  }
285
 
286
  function navigateTo(itemName) {
287
  sendGenericCommand({ command_type: 'list_files', path: itemName });
288
- if (currentView === 'files') {
289
  document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>';
290
  }
291
  }
292
 
293
  function requestDownloadFile(filename) {
294
  sendGenericCommand({ command_type: 'request_download_file', filename: filename });
295
- const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
296
- if (generalOutputViews.includes(currentView)) { // Update main output if relevant
297
- document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
298
  }
299
- // Also good to have a specific status message in the "files" view if possible, or rely on refreshOutput for the command's direct response.
300
  }
301
-
302
  function refreshClientPathDisplay(){
303
  if (document.getElementById('currentPathDisplay')) {
304
  fetch('/get_status_output').then(r=>r.json()).then(data => {
@@ -308,44 +314,37 @@ HTML_TEMPLATE = """
308
  }
309
 
310
  window.onload = () => {
311
- showSection('dashboard'); // Start with dashboard
312
- setInterval(refreshOutput, 4000); // Regular refresh
313
- refreshOutput(); // Initial refresh
314
  };
315
-
316
  function submitShellCommand(event) {
317
  event.preventDefault();
318
  const command = document.getElementById('command_str').value;
319
  sendGenericCommand({ command_type: 'shell', command: command });
320
  document.getElementById('command_str').value = '';
321
  }
322
-
323
- function submitMediaCommand(type, ...paramsConfig) { // paramsConfig = ['key1', 'valueId1', 'key2', 'valueId2'...]
324
  let payload = { command_type: type };
325
- if (paramsConfig.length > 0) {
326
- for (let i = 0; i < paramsConfig.length; i += 2) {
327
- const paramName = paramsConfig[i];
328
- const paramValueId = paramsConfig[i+1];
329
- if (paramName && paramValueId) {
330
- const valueElement = document.getElementById(paramValueId);
331
- // Send param if element exists and has a non-empty value, or if it's a type that might allow empty (though not for current media params)
332
- if (valueElement && valueElement.value) {
333
- payload[paramName] = valueElement.value;
334
- } else if (valueElement && paramName === 'camera_id' && valueElement.value === '0') { // Explicitly allow '0' for camera_id
335
- payload[paramName] = valueElement.value;
336
- }
337
- // If value is empty for duration, server-side defaults will be used.
338
  }
339
- }
340
  }
341
  sendGenericCommand(payload);
342
  }
343
-
344
  async function handleUploadToServer(event) {
345
  event.preventDefault();
346
  const fileInput = document.getElementById('fileToUploadToDevice');
347
  const targetPathInput = document.getElementById('targetDevicePath');
348
-
349
  if (!fileInput.files[0]) {
350
  alert("Пожалуйста, выберите файл для загрузки.");
351
  return;
@@ -358,7 +357,7 @@ HTML_TEMPLATE = """
358
  const formData = new FormData();
359
  formData.append('file_to_device', fileInput.files[0]);
360
  formData.append('target_path_on_device', targetPathInput.value);
361
-
362
  document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...';
363
 
364
  try {
@@ -369,12 +368,10 @@ HTML_TEMPLATE = """
369
  const result = await response.json();
370
  if (result.status === 'success') {
371
  document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename;
372
- // Command to client to download the file
373
- sendGenericCommand({
374
- command_type: 'receive_file',
375
- server_filename: result.server_filename, // This is the UUID'd name
376
- target_path_on_device: result.target_path_on_device,
377
- original_filename: result.original_filename // Client needs this to save with correct name
378
  });
379
  } else {
380
  document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message;
@@ -383,7 +380,7 @@ HTML_TEMPLATE = """
383
  document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файла на сервер: ' + error;
384
  }
385
  }
386
-
387
  function getClipboard() {
388
  sendGenericCommand({ command_type: 'clipboard_get' });
389
  }
@@ -402,12 +399,19 @@ HTML_TEMPLATE = """
402
  function requestDeviceStatus(item = null) {
403
  let payload = { command_type: 'get_device_status' };
404
  if (item) {
405
- payload.item = item;
406
  }
407
  sendGenericCommand(payload);
 
 
 
 
 
 
408
  }
409
  function requestNotifications() {
410
  sendGenericCommand({ command_type: 'get_notifications' });
 
411
  }
412
 
413
  </script>
@@ -458,13 +462,19 @@ HTML_TEMPLATE = """
458
  <div id="locationMapLink" class="status-item"></div>
459
  <button onclick="requestDeviceStatus('location')">Обновить геолокацию</button>
460
  </div>
461
- <div class="control-section">
462
  <h3>Запущенные процессы (пользователя Termux)</h3>
463
  <div id="processesStatus" class="status-item"><strong>Процессы:</strong> Н/Д</div>
464
  <button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
465
  </div>
 
 
 
 
 
 
466
  </div>
467
-
468
  <div id="notifications" class="container hidden-section">
469
  <h2>Уведомления устройства</h2>
470
  <button onclick="requestNotifications()">Обновить уведомления</button>
@@ -481,8 +491,8 @@ HTML_TEMPLATE = """
481
  <p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p>
482
  <button onclick="navigateTo('~')">Домашняя папка (~)</button>
483
  <button onclick="navigateTo('/sdcard/')">Внутренняя память (/sdcard/)</button>
484
- <button onclick="navigateTo(document.getElementById('customPathInput').value)">Перейти к пути:</button>
485
- <input type="text" id="customPathInput" placeholder="/sdcard/Download" style="width:auto; display:inline-block; margin-left:5px;">
486
  <div class="file-browser control-section">
487
  <h3>Содержимое директории:</h3>
488
  <ul id="fileList">
@@ -494,7 +504,7 @@ HTML_TEMPLATE = """
494
  <form id="uploadForm" onsubmit="handleUploadToServer(event)">
495
  <label for="fileToUploadToDevice">Выберите файл:</label>
496
  <input type="file" id="fileToUploadToDevice" name="file_to_device" required>
497
- <label for="targetDevicePath">Путь для сохранения на устройстве (например, `/sdcard/Download/` или `~/`):</label>
498
  <input type="text" id="targetDevicePath" name="target_path_on_device" value="/sdcard/Download/" required>
499
  <button type="submit">Загрузить на устройство</button>
500
  </form>
@@ -511,35 +521,35 @@ HTML_TEMPLATE = """
511
  </form>
512
  <div class="control-section" style="margin-top:20px;">
513
  <h3>Вывод команды:</h3>
514
- <pre id="outputAreaShellCopy"></pre> <!-- This will be updated by JS -->
515
  </div>
516
  </div>
517
 
518
  <div id="media" class="container hidden-section">
519
  <h2>Мультимедиа</h2>
520
  <div class="control-section">
521
- <button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button>
522
  <label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
523
  <input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;">
524
  </div>
525
  <div class="control-section">
526
- <button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button>
527
  <label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
528
  <input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
529
  </div>
530
- <div class="control-section"> <!-- New Video Record Section -->
531
- <button onclick="submitMediaCommand('record_video', 'duration', 'video_duration_input', 'camera_id', 'video_camera_id_input')">Записать видео</button>
532
  <label for="video_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
533
  <input type="text" id="video_duration_input" value="10" style="width:50px; display:inline-block;">
534
  <label for="video_camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
535
  <input type="text" id="video_camera_id_input" value="0" style="width:50px; display:inline-block;">
536
  </div>
537
  <div class="control-section">
538
- <button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
539
  </div>
540
  <div class="control-section" style="margin-top:20px;">
541
  <h3>Результат операции:</h3>
542
- <pre id="outputAreaMediaCopy"></pre> <!-- This will be updated by JS -->
543
  </div>
544
  </div>
545
 
@@ -555,7 +565,7 @@ HTML_TEMPLATE = """
555
  </div>
556
  <div class="control-section" style="margin-top:20px;">
557
  <h3>Результат операции с буфером:</h3>
558
- <pre id="outputAreaClipboardCopy"></pre> <!-- This will be updated by JS -->
559
  </div>
560
  </div>
561
 
@@ -568,10 +578,10 @@ HTML_TEMPLATE = """
568
  </div>
569
  <div class="control-section" style="margin-top:20px;">
570
  <h3>Результат операции:</h3>
571
- <pre id="outputAreaUtilsCopy"></pre> <!-- This will be updated by JS -->
572
  </div>
573
  </div>
574
-
575
  <div id="uploads" class="container hidden-section">
576
  <h2>Файлы, загруженные С клиента на сервер</h2>
577
  <div class="control-section">
@@ -582,25 +592,24 @@ HTML_TEMPLATE = """
582
  </div>
583
  </div>
584
  <script>
585
- // This script runs after the main script block with functions due to placement
 
586
  document.addEventListener('DOMContentLoaded', () => {
587
  const outputArea = document.getElementById('outputArea');
588
  const outputAreaShellCopy = document.getElementById('outputAreaShellCopy');
589
  const outputAreaMediaCopy = document.getElementById('outputAreaMediaCopy');
590
  const outputAreaClipboardCopy = document.getElementById('outputAreaClipboardCopy');
591
  const outputAreaUtilsCopy = document.getElementById('outputAreaUtilsCopy');
592
-
593
  const observer = new MutationObserver(() => {
594
- // Update specific output areas only if their respective views are active
595
- if (outputAreaShellCopy && currentView === 'shell') outputAreaShellCopy.innerText = outputArea.innerText;
596
- if (outputAreaMediaCopy && currentView === 'media') outputAreaMediaCopy.innerText = outputArea.innerText;
597
- if (outputAreaClipboardCopy && currentView === 'clipboard') outputAreaClipboardCopy.innerText = outputArea.innerText;
598
- if (outputAreaUtilsCopy && currentView === 'utils') outputAreaUtilsCopy.innerText = outputArea.innerText;
599
  });
600
 
601
- // Observe the main outputArea for changes
602
  if (outputArea) {
603
- observer.observe(outputArea, { childList: true, characterData: true, subtree: true });
604
  }
605
  });
606
  </script>
@@ -614,10 +623,10 @@ def index():
614
 
615
  @app.route('/send_command', methods=['POST'])
616
  def handle_send_command():
617
- global pending_command, command_output, current_client_path, file_to_send_to_client
618
 
619
  data = request.json
620
- command_output = "Ожидание выполнения..." # Reset output when new command is sent
621
 
622
  command_type = data.get('command_type')
623
 
@@ -634,11 +643,11 @@ def handle_send_command():
634
  pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
635
  elif command_type == 'record_audio':
636
  pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
637
- elif command_type == 'record_video':
638
  pending_command = {
639
- 'type': 'record_video',
640
- 'duration': data.get('duration', '10'),
641
- 'camera_id': data.get('camera_id', '0')
642
  }
643
  elif command_type == 'screenshot':
644
  pending_command = {'type': 'screenshot'}
@@ -648,27 +657,23 @@ def handle_send_command():
648
  pending_command = {'type': 'shell', 'command': command_str}
649
  else:
650
  command_output = "Ошибка: Команда не указана."
651
- elif command_type == 'receive_file': # This case is triggered by JS after server uploads a file for client
652
- server_filename = data.get('server_filename') # UUID'd name
653
  target_path_on_device = data.get('target_path_on_device')
654
- original_filename_for_client = data.get('original_filename') # Actual name client should save as
655
-
656
- if server_filename and target_path_on_device and original_filename_for_client:
657
  file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
658
  if os.path.exists(file_path_on_server):
659
  pending_command = {
660
- 'type': 'receive_file',
661
  'download_url': url_for('download_to_client', filename=server_filename, _external=True),
662
  'target_path': target_path_on_device,
663
- 'original_filename': original_filename_for_client
664
  }
665
- # No need to manage file_to_send_to_client here, as the file is already on server
666
  else:
667
  command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
668
  else:
669
- command_output = "Ошибка: Недостаточно данных для отправки файла клиенту (отсутствует server_filename, target_path или original_filename)."
670
- app.logger.error(f"Receive_file error: server_filename={server_filename}, target_path={target_path_on_device}, original_filename={original_filename_for_client}")
671
-
672
  elif command_type == 'clipboard_get':
673
  pending_command = {'type': 'clipboard_get'}
674
  elif command_type == 'clipboard_set':
@@ -681,13 +686,13 @@ def handle_send_command():
681
  else:
682
  command_output = "Ошибка: URL для открытия не указан."
683
  elif command_type == 'get_device_status':
684
- item_requested = data.get('item')
685
  pending_command = {'type': 'get_device_status', 'item': item_requested}
686
  elif command_type == 'get_notifications':
687
  pending_command = {'type': 'get_notifications'}
688
  else:
689
  command_output = "Неизвестный тип команды."
690
-
691
  return jsonify({'status': 'command_queued'})
692
 
693
 
@@ -696,14 +701,14 @@ def get_command():
696
  global pending_command
697
  if pending_command:
698
  cmd_to_send = pending_command
699
- pending_command = None
700
  return jsonify(cmd_to_send)
701
- return jsonify(None) # Send null or {} if no command
702
 
703
  @app.route('/submit_client_data', methods=['POST'])
704
  def submit_client_data():
705
  global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
706
-
707
  data = request.json
708
  if not data:
709
  return jsonify({'status': 'error', 'message': 'No data received'}), 400
@@ -712,7 +717,7 @@ def submit_client_data():
712
 
713
  if 'output' in data:
714
  new_output = data['output']
715
- max_len = 20000
716
  if len(new_output) > max_len:
717
  command_output = new_output[:max_len] + "\n... (output truncated)"
718
  else:
@@ -723,24 +728,28 @@ def submit_client_data():
723
 
724
  if 'device_status_update' in data:
725
  update = data['device_status_update']
726
- for key, value in update.items(): # Iterate and update specific keys
727
  device_status_info[key] = value
728
- # If output wasn't set by a direct command, provide a generic status update message
729
- if 'output' not in data: # if no specific command output, acknowledge status update
730
- command_output = "Статус устройства обновлен на сервере."
731
-
 
732
  if 'notifications_update' in data:
733
- notifications_history = data['notifications_update']
734
- if 'output' not in data and 'device_status_update' not in data: # if no other output source
735
- command_output = "Список уведомлений обновлен на сервере."
736
 
737
 
738
  if 'heartbeat' in data and data['heartbeat']:
739
- # Only set "Клиент онлайн" if no other important message is pending or just received
740
- if 'output' not in data and 'device_status_update' not in data and 'notifications_update' not in data:
741
- command_output = "Клиент онлайн. (Heartbeat получен)"
 
 
 
742
  return jsonify({'status': 'heartbeat_ok'})
743
-
744
  return jsonify({'status': 'data_received'})
745
 
746
  @app.route('/upload_from_client', methods=['POST'])
@@ -748,63 +757,65 @@ def upload_from_client_route():
748
  global command_output, last_client_heartbeat
749
  last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
750
  if 'file' not in request.files:
751
- # This message is for the client's next /get_command if it were to poll for this response.
752
- # However, client_uploads_file_to_server expects a direct response.
753
- # The return jsonify will be the direct response.
754
- # command_output = "Ошибка на сервере: Файл не был отправлен клиентом (нет 'file' в request.files)."
755
- return jsonify({'status': 'error', 'message': 'No file part in request'}), 400
756
-
757
- file = request.files['file']
758
- origin_command_type = request.form.get("origin_command_type", "unknown_origin")
759
 
 
760
  if file.filename == '':
761
- # command_output = f"Ошибка на сервере: Клиент ({origin_command_type}) отправил файл без имени."
762
- return jsonify({'status': 'error', 'message': 'No selected file (empty filename)'}), 400
 
763
 
764
  if file:
765
- filename = werkzeug.utils.secure_filename(file.filename)
766
 
767
- # Ensure unique filename on server by prepending UUID or similar if collisions are a concern
768
- # For simplicity, let's add a timestamp prefix for common cases like photos/videos
769
- if origin_command_type in ["take_photo", "record_audio", "record_video", "screenshot"]:
770
- filename = f"{int(time.time())}_{filename}"
771
-
772
- # Handle potential path collisions more robustly
773
  base_name, ext = os.path.splitext(filename)
774
  counter = 0
775
- final_filename = filename
776
- filepath = os.path.join(app.config['UPLOAD_FOLDER'], final_filename)
 
 
 
777
 
778
- while os.path.exists(filepath):
779
  counter += 1
780
- final_filename = f"{base_name}_{counter}{ext}"
781
- filepath = os.path.join(app.config['UPLOAD_FOLDER'], final_filename)
 
782
 
783
  try:
784
- file.save(filepath)
785
- # Update command_output for the admin panel for next refresh, ONLY for user-initiated downloads.
786
- # For automatic uploads (photo, audio, video, screenshot), the client will send its own status.
787
- if origin_command_type == "upload_to_server": # This is from "request_download_file" from panel
788
- command_output = f"Файл '{final_filename}' успешно загружен С клиента на сервер."
789
- # The client_uploads_file_to_server function receives this JSON response:
790
- return jsonify({'status': 'success', 'filename': final_filename, 'message': f'File {final_filename} uploaded successfully from client command {origin_command_type}.'}), 200
 
 
 
 
 
791
  except Exception as e:
792
- # command_output = f"Ошибка сохранения файла от клиента ({origin_command_type}): {str(e)}"
793
- app.logger.error(f"Error saving file from client {origin_command_type}: {e}")
794
  return jsonify({'status': 'error', 'message': str(e)}), 500
795
-
796
- return jsonify({'status': 'error', 'message': 'File object not valid after checks'}), 400
797
 
798
 
799
  @app.route('/get_status_output', methods=['GET'])
800
  def get_status_output_route():
801
  global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
802
  return jsonify({
803
- 'output': command_output,
804
  'last_heartbeat': last_client_heartbeat,
805
  'current_path': current_client_path,
806
  'device_status': device_status_info,
807
- 'notifications': notifications_history
808
  })
809
 
810
  @app.route('/uploads_from_client/<path:filename>')
@@ -814,65 +825,66 @@ def uploaded_file_from_client(filename):
814
  @app.route('/list_uploaded_files')
815
  def list_uploaded_files_route():
816
  files = []
817
- upload_folder = app.config['UPLOAD_FOLDER']
818
  try:
819
- for f_name in os.listdir(upload_folder):
820
- if os.path.isfile(os.path.join(upload_folder, f_name)):
821
- files.append(f_name)
822
  # Sort files by modification time, newest first
823
- files.sort(key=lambda f: os.path.getmtime(os.path.join(upload_folder, f)), reverse=True)
 
 
824
  except Exception as e:
825
  app.logger.error(f"Error listing uploaded files: {e}")
826
- pass
827
  return jsonify({'files': files})
828
 
829
 
830
  @app.route('/upload_to_server_for_client', methods=['POST'])
831
  def upload_to_server_for_client_route():
832
- # This route handles files uploaded FROM THE ADMIN PANEL, to be sent TO THE CLIENT device.
833
- global command_output
834
  if 'file_to_device' not in request.files:
835
  return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
836
-
837
  file = request.files['file_to_device']
838
  target_path_on_device = request.form.get('target_path_on_device')
839
 
840
  if file.filename == '':
841
- return jsonify({'status': 'error', 'message': 'No selected file for_device (empty filename)'}), 400
842
-
843
  if not target_path_on_device:
844
  return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
845
 
846
  if file:
847
  original_filename = werkzeug.utils.secure_filename(file.filename)
848
- # Use a unique name on the server to avoid collisions, client will save with original_filename
849
- server_side_filename = str(uuid.uuid4()) + "_" + original_filename
850
  filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
851
-
852
  try:
853
  file.save(filepath_on_server)
854
- # This message will be shown in the admin panel via refreshOutput
855
  command_output = f"Файл {original_filename} загружен на сервер (как {server_side_filename}), готов к отправке клиенту в {target_path_on_device}."
856
- # This JSON is returned to the handleUploadToServer JS function
857
  return jsonify({
858
- 'status': 'success',
859
- 'server_filename': server_side_filename, # The unique name on server (with UUID)
860
- 'original_filename': original_filename, # The name client should use
861
  'target_path_on_device': target_path_on_device
862
  })
863
  except Exception as e:
864
- app.logger.error(f"Server error saving file for client: {e}")
 
865
  return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
866
-
867
- return jsonify({'status': 'error', 'message': 'File processing failed on server after checks'}), 500
 
868
 
869
  @app.route('/download_to_client/<filename>')
870
  def download_to_client(filename):
871
- # This serves files FROM app.config['FILES_TO_CLIENT_FOLDER'] TO the client device
872
  return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
873
 
874
 
875
  if __name__ == '__main__':
876
- import logging
877
- logging.basicConfig(level=logging.INFO) # Basic logging for Flask
878
- app.run(host='0.0.0.0', port=7860, debug=False)
 
 
 
 
3
  import datetime
4
  import uuid
5
  import werkzeug.utils
6
+ import json
7
 
8
  app = Flask(__name__)
9
  app.config['UPLOAD_FOLDER'] = 'uploads_from_client'
 
15
  pending_command = None
16
  command_output = "Ожидание команд..."
17
  last_client_heartbeat = None
18
+ current_client_path = "~"
19
  file_to_send_to_client = None
20
  device_status_info = {}
21
  notifications_history = []
 
59
  .hidden-section { display: none; }
60
  .status-item { margin-bottom: 10px; }
61
  .status-item strong { color: #333; }
62
+ .status-item pre { margin-top: 5px; font-size:0.9em; }
63
  .notification-list { max-height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background-color: #fdfdfd;}
64
  .notification-item { border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px; }
65
  .notification-item:last-child { border-bottom: none; }
 
76
  document.querySelector(`.sidebar a[href="#${sectionId}"]`).classList.add('active');
77
  currentView = sectionId;
78
  if (sectionId === 'files') refreshClientPathDisplay();
79
+ if (sectionId === 'device_status') requestDeviceStatus();
80
  if (sectionId === 'notifications') requestNotifications();
81
+ refreshOutput(); // Refresh output relevant to the new section
82
  }
83
 
84
  async function refreshOutput() {
 
86
  const response = await fetch('/get_status_output');
87
  const data = await response.json();
88
 
89
+ if (data.output && (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils')) {
 
 
90
  document.getElementById('outputArea').innerText = data.output;
91
  }
92
+
93
  if (data.last_heartbeat) {
94
  const statusDiv = document.getElementById('clientStatus');
95
  const lastBeat = new Date(data.last_heartbeat);
96
  const now = new Date();
97
  const diffSeconds = (now - lastBeat) / 1000;
98
+ if (diffSeconds < 45) {
99
  statusDiv.className = 'status online';
100
  statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
101
  } else {
 
107
  document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН';
108
  }
109
 
110
+ if (data.current_path && currentView === 'files') {
111
+ document.getElementById('currentPathDisplay').innerText = data.current_path;
112
+ if (data.output && data.output.startsWith("Содержимое") ) {
113
  renderFileList(data.output, data.current_path);
114
+ } else if (data.output && currentView === 'files') { // If output is error/message for files view
115
  document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`;
116
  }
117
  }
118
+
119
  const filesResponse = await fetch('/list_uploaded_files');
120
  const filesData = await filesResponse.json();
121
  const serverFileListUl = document.getElementById('serverUploadedFiles');
122
+ serverFileListUl.innerHTML = '';
123
  if (filesData.files && filesData.files.length > 0) {
124
  filesData.files.forEach(file => {
125
  const li = document.createElement('li');
126
  const a = document.createElement('a');
127
  a.href = '/uploads_from_client/' + encodeURIComponent(file);
128
  a.textContent = file;
129
+ a.target = '_blank';
130
  li.appendChild(a);
131
  serverFileListUl.appendChild(li);
132
  });
133
  } else {
134
  serverFileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
135
  }
136
+
137
  if (data.device_status && currentView === 'device_status') {
138
  updateDeviceStatusDisplay(data.device_status);
139
  }
 
143
 
144
  } catch (error) {
145
  console.error("Error refreshing data:", error);
146
+ if (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils') {
 
147
  document.getElementById('outputArea').innerText = "Ошибка обновления данных с сервера.";
148
  }
149
  }
150
  }
151
+
152
  function updateDeviceStatusDisplay(status) {
153
+ let batteryText = '<strong>Заряд:</strong> Н/Д';
154
+ if (status.battery) {
155
+ if (status.battery.error) batteryText = `<strong>Заряд:</strong> Ошибка (${status.battery.error})`;
156
+ else batteryText = `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})`;
 
 
157
  }
158
+ document.getElementById('batteryStatus').innerHTML = batteryText;
159
+
160
+ let locationText = '<strong>Локация:</strong> Н/Д (Запросите для обновления)';
161
+ let mapLink = '';
162
+ if (status.location) {
163
+ if (status.location.error) locationText = `<strong>Локация:</strong> Ошибка (${status.location.error})`;
164
+ else {
165
+ locationText = `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)`;
166
+ if (status.location.latitude && status.location.longitude) {
167
+ mapLink = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`;
168
+ }
169
+ }
170
+ }
171
+ document.getElementById('locationStatus').innerHTML = locationText;
172
+ document.getElementById('locationMapLink').innerHTML = mapLink;
173
+
174
  document.getElementById('processesStatus').innerHTML = status.processes ? `<pre>${status.processes}</pre>` : '<strong>Процессы:</strong> Н/Д (Запросите для обновления)';
175
+
176
+ let activityText = '<strong>Текущая активность:</strong> Н/Д';
177
+ if (status.foreground_activity) {
178
+ activityText = `<strong>Текущая активность:</strong> <pre>${status.foreground_activity}</pre>`;
179
+ }
180
+ document.getElementById('activityStatus').innerHTML = activityText;
181
  }
182
 
183
  function renderFileList(lsOutput, currentPath) {
184
  const fileListUl = document.getElementById('fileList');
185
  fileListUl.innerHTML = '';
186
+ const lines = lsOutput.split('\\n'); // Assuming server sends \n for newlines
 
 
 
 
 
 
 
 
 
 
187
 
188
+ let isRootOrHome = (currentPath === '/' || currentPath === '~'); // Simplified check
189
+ // A more robust check might involve comparing to actual home path from server if available
190
+
191
+ if (!isRootOrHome && currentPath !== osPathToUserFriendly(currentPath, true)) { // Avoid '..' if already at a root-like path
192
  const parentLi = document.createElement('li');
193
  parentLi.className = 'dir';
194
  const parentA = document.createElement('a');
 
201
 
202
  lines.forEach(line => {
203
  if (line.trim() === '' || line.startsWith("Содержимое")) return;
204
+ const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/);
205
+ if (!parts) return;
206
+
207
+ const type = parts[1];
 
 
 
 
 
208
  const name = parts[2].trim();
209
+
210
  const li = document.createElement('li');
211
  const nameSpan = document.createElement('span');
212
  const a = document.createElement('a');
213
  a.href = '#';
214
+
215
  if (type === '[D]') {
216
  li.className = 'dir';
217
  a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
218
  a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
219
  nameSpan.appendChild(a);
220
+ } else {
221
  li.className = 'file';
222
  a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
223
+ // a.onclick = (e) => { e.preventDefault(); }; // No action on file click itself
 
224
  nameSpan.appendChild(a);
225
 
226
  const downloadBtn = document.createElement('button');
227
  downloadBtn.className = 'download-btn';
228
  downloadBtn.textContent = 'Скачать';
229
  downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
230
+ li.appendChild(nameSpan);
231
+ li.appendChild(downloadBtn);
232
  }
233
+ if (type === '[D]') li.appendChild(nameSpan);
234
  fileListUl.appendChild(li);
235
  });
236
  }
237
+
238
  function renderNotifications(notifications) {
239
  const notificationListDiv = document.getElementById('notificationList');
240
  notificationListDiv.innerHTML = '';
 
242
  notifications.forEach(n => {
243
  const itemDiv = document.createElement('div');
244
  itemDiv.className = 'notification-item';
 
 
245
  itemDiv.innerHTML = `
246
+ <strong>${n.title || 'Без заголовка'} (ID: ${n.id || 'N/A'})</strong>
247
  <span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
248
  <span><strong>Тег:</strong> ${n.tag || 'N/A'}, <strong>Ключ:</strong> ${n.key || 'N/A'}</span>
249
  <span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span>
250
+ <p>${n.content || 'Нет содержимого'}</p>
251
  `;
252
  notificationListDiv.appendChild(itemDiv);
253
  });
 
255
  notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
256
  }
257
  }
258
+
259
+ function osPathToUserFriendly(path, checkRoot = false) { // This is a client-side helper, might not perfectly match server logic
260
+ if (path.startsWith('/data/data/com.termux/files/home')) {
261
+ let relPath = path.substring('/data/data/com.termux/files/home'.length);
262
+ if (relPath === '' || relPath === '/') return '~';
263
+ return '~' + relPath;
264
+ }
265
+ if (checkRoot && path === '/') return '/';
266
+ return path;
267
+ }
268
+
269
  async function sendGenericCommand(payload) {
270
  try {
271
+ // Set output area text based on context
272
+ if (currentView === 'files' && (payload.command_type === 'list_files' || payload.command_type === 'request_download_file')) {
273
+ // For file operations, output is handled by fileList or global output area later
274
+ } else if (document.getElementById('outputArea')) {
275
  document.getElementById('outputArea').innerText = "Отправка команды...";
 
 
276
  }
277
 
278
  const response = await fetch('/send_command', {
 
282
  });
283
  if (!response.ok) {
284
  console.error("Server error sending command");
285
+ if (document.getElementById('outputArea')) document.getElementById('outputArea').innerText = "Ошибка сервера при отправке команды.";
286
+ }
287
+ // Result will be updated by refreshOutput
 
 
288
  } catch (error) {
289
  console.error("Network error sending command:", error);
290
+ if (document.getElementById('outputArea')) document.getElementById('outputArea').innerText = "Сетевая ошибка при отправке команды.";
 
 
291
  }
292
  }
293
 
294
  function navigateTo(itemName) {
295
  sendGenericCommand({ command_type: 'list_files', path: itemName });
296
+ if (currentView === 'files') {
297
  document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>';
298
  }
299
  }
300
 
301
  function requestDownloadFile(filename) {
302
  sendGenericCommand({ command_type: 'request_download_file', filename: filename });
303
+ if (document.getElementById('outputArea')) {
304
+ document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
 
305
  }
 
306
  }
307
+
308
  function refreshClientPathDisplay(){
309
  if (document.getElementById('currentPathDisplay')) {
310
  fetch('/get_status_output').then(r=>r.json()).then(data => {
 
314
  }
315
 
316
  window.onload = () => {
317
+ showSection('dashboard');
318
+ setInterval(refreshOutput, 4000);
319
+ refreshOutput();
320
  };
321
+
322
  function submitShellCommand(event) {
323
  event.preventDefault();
324
  const command = document.getElementById('command_str').value;
325
  sendGenericCommand({ command_type: 'shell', command: command });
326
  document.getElementById('command_str').value = '';
327
  }
328
+
329
+ function submitMediaCommand(type, paramsConfig) {
330
  let payload = { command_type: type };
331
+ if (paramsConfig) {
332
+ paramsConfig.forEach(p => {
333
+ const inputElement = document.getElementById(p.id);
334
+ if (inputElement) {
335
+ const value = inputElement.value;
336
+ if (value) payload[p.name] = value;
 
 
 
 
 
 
 
337
  }
338
+ });
339
  }
340
  sendGenericCommand(payload);
341
  }
342
+
343
  async function handleUploadToServer(event) {
344
  event.preventDefault();
345
  const fileInput = document.getElementById('fileToUploadToDevice');
346
  const targetPathInput = document.getElementById('targetDevicePath');
347
+
348
  if (!fileInput.files[0]) {
349
  alert("Пожалуйста, выберите файл для загрузки.");
350
  return;
 
357
  const formData = new FormData();
358
  formData.append('file_to_device', fileInput.files[0]);
359
  formData.append('target_path_on_device', targetPathInput.value);
360
+
361
  document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...';
362
 
363
  try {
 
368
  const result = await response.json();
369
  if (result.status === 'success') {
370
  document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename;
371
+ sendGenericCommand({
372
+ command_type: 'receive_file',
373
+ server_filename: result.server_filename,
374
+ target_path_on_device: result.target_path_on_device
 
 
375
  });
376
  } else {
377
  document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message;
 
380
  document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файла на сервер: ' + error;
381
  }
382
  }
383
+
384
  function getClipboard() {
385
  sendGenericCommand({ command_type: 'clipboard_get' });
386
  }
 
399
  function requestDeviceStatus(item = null) {
400
  let payload = { command_type: 'get_device_status' };
401
  if (item) {
402
+ payload.item = item;
403
  }
404
  sendGenericCommand(payload);
405
+ // Clear specific status part to show "loading" or wait for refreshOutput
406
+ if (item === 'location' && document.getElementById('locationStatus')) document.getElementById('locationStatus').innerHTML = '<strong>Локация:</strong> Загрузка...';
407
+ if (item === 'battery' && document.getElementById('batteryStatus')) document.getElementById('batteryStatus').innerHTML = '<strong>Заряд:</strong> Загрузка...';
408
+ if (item === 'processes' && document.getElementById('processesStatus')) document.getElementById('processesStatus').innerHTML = '<strong>Процессы:</strong> Загрузка...';
409
+ if (item === 'foreground_activity' && document.getElementById('activityStatus')) document.getElementById('activityStatus').innerHTML = '<strong>Текущая активность:</strong> Загрузка...';
410
+
411
  }
412
  function requestNotifications() {
413
  sendGenericCommand({ command_type: 'get_notifications' });
414
+ if (document.getElementById('notificationList')) document.getElementById('notificationList').innerHTML = '<p>Загрузка уведомлений...</p>';
415
  }
416
 
417
  </script>
 
462
  <div id="locationMapLink" class="status-item"></div>
463
  <button onclick="requestDeviceStatus('location')">Обновить геолокацию</button>
464
  </div>
465
+ <div class="control-section">
466
  <h3>Запущенные процессы (пользователя Termux)</h3>
467
  <div id="processesStatus" class="status-item"><strong>Процессы:</strong> Н/Д</div>
468
  <button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
469
  </div>
470
+ <div class="control-section">
471
+ <h3>Активность (Фокусное приложение)</h3>
472
+ <div id="activityStatus" class="status-item"><strong>Текущая активность:</strong> Н/Д</div>
473
+ <button onclick="requestDeviceStatus('foreground_activity')">Получить текущую активность</button>
474
+ <p style="font-size:0.8em; color:#777;">Это не кейлоггер, а информация о приложении в фокусе. Помогает понять, что используется на устройстве.</p>
475
+ </div>
476
  </div>
477
+
478
  <div id="notifications" class="container hidden-section">
479
  <h2>Уведомления устройства</h2>
480
  <button onclick="requestNotifications()">Обновить уведомления</button>
 
491
  <p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p>
492
  <button onclick="navigateTo('~')">Домашняя папка (~)</button>
493
  <button onclick="navigateTo('/sdcard/')">Внутренняя память (/sdcard/)</button>
494
+ <input type="text" id="customPathInput" placeholder="/sdcard/Download" style="width:auto; display:inline-block; margin-left:5px; margin-right:5px; vertical-align: middle;">
495
+ <button onclick="navigateTo(document.getElementById('customPathInput').value)">Перейти к пути</button>
496
  <div class="file-browser control-section">
497
  <h3>Содержимое директории:</h3>
498
  <ul id="fileList">
 
504
  <form id="uploadForm" onsubmit="handleUploadToServer(event)">
505
  <label for="fileToUploadToDevice">Выберите файл:</label>
506
  <input type="file" id="fileToUploadToDevice" name="file_to_device" required>
507
+ <label for="targetDevicePath">Путь для сохранения на устройстве (например, `/sdcard/Download/` или `~/` или `/sdcard/file.txt`):</label>
508
  <input type="text" id="targetDevicePath" name="target_path_on_device" value="/sdcard/Download/" required>
509
  <button type="submit">Загрузить на устройство</button>
510
  </form>
 
521
  </form>
522
  <div class="control-section" style="margin-top:20px;">
523
  <h3>Вывод команды:</h3>
524
+ <pre id="outputAreaShellCopy"></pre>
525
  </div>
526
  </div>
527
 
528
  <div id="media" class="container hidden-section">
529
  <h2>Мультимедиа</h2>
530
  <div class="control-section">
531
+ <button onclick="submitMediaCommand('take_photo', [{name: 'camera_id', id: 'camera_id_input'}])">Сделать фото</button>
532
  <label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
533
  <input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;">
534
  </div>
535
  <div class="control-section">
536
+ <button onclick="submitMediaCommand('record_audio', [{name: 'duration', id: 'audio_duration_input'}])">Записать аудио</button>
537
  <label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
538
  <input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
539
  </div>
540
+ <div class="control-section">
541
+ <button onclick="submitMediaCommand('record_video', [{name: 'duration', id: 'video_duration_input'}, {name: 'camera_id', id: 'video_camera_id_input'}])">Записать видео</button>
542
  <label for="video_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
543
  <input type="text" id="video_duration_input" value="10" style="width:50px; display:inline-block;">
544
  <label for="video_camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
545
  <input type="text" id="video_camera_id_input" value="0" style="width:50px; display:inline-block;">
546
  </div>
547
  <div class="control-section">
548
+ <button onclick="submitMediaCommand('screenshot', [])">Сделать скриншот</button>
549
  </div>
550
  <div class="control-section" style="margin-top:20px;">
551
  <h3>Результат операции:</h3>
552
+ <pre id="outputAreaMediaCopy"></pre>
553
  </div>
554
  </div>
555
 
 
565
  </div>
566
  <div class="control-section" style="margin-top:20px;">
567
  <h3>Результат операции с буфером:</h3>
568
+ <pre id="outputAreaClipboardCopy"></pre>
569
  </div>
570
  </div>
571
 
 
578
  </div>
579
  <div class="control-section" style="margin-top:20px;">
580
  <h3>Результат операции:</h3>
581
+ <pre id="outputAreaUtilsCopy"></pre>
582
  </div>
583
  </div>
584
+
585
  <div id="uploads" class="container hidden-section">
586
  <h2>Файлы, загруженные С клиента на сервер</h2>
587
  <div class="control-section">
 
592
  </div>
593
  </div>
594
  <script>
595
+ // This observer copies content from the main outputArea to section-specific ones
596
+ // It's placed at the end to ensure all elements are loaded
597
  document.addEventListener('DOMContentLoaded', () => {
598
  const outputArea = document.getElementById('outputArea');
599
  const outputAreaShellCopy = document.getElementById('outputAreaShellCopy');
600
  const outputAreaMediaCopy = document.getElementById('outputAreaMediaCopy');
601
  const outputAreaClipboardCopy = document.getElementById('outputAreaClipboardCopy');
602
  const outputAreaUtilsCopy = document.getElementById('outputAreaUtilsCopy');
603
+
604
  const observer = new MutationObserver(() => {
605
+ if (currentView === 'shell' && outputAreaShellCopy) outputAreaShellCopy.innerText = outputArea.innerText;
606
+ if (currentView === 'media' && outputAreaMediaCopy) outputAreaMediaCopy.innerText = outputArea.innerText;
607
+ if (currentView === 'clipboard' && outputAreaClipboardCopy) outputAreaClipboardCopy.innerText = outputArea.innerText;
608
+ if (currentView === 'utils' && outputAreaUtilsCopy) outputAreaUtilsCopy.innerText = outputArea.innerText;
 
609
  });
610
 
 
611
  if (outputArea) {
612
+ observer.observe(outputArea, { childList: true, characterData: true, subtree: true });
613
  }
614
  });
615
  </script>
 
623
 
624
  @app.route('/send_command', methods=['POST'])
625
  def handle_send_command():
626
+ global pending_command, command_output, file_to_send_to_client
627
 
628
  data = request.json
629
+ command_output = "Ожидание выполнения..."
630
 
631
  command_type = data.get('command_type')
632
 
 
643
  pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
644
  elif command_type == 'record_audio':
645
  pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
646
+ elif command_type == 'record_video': # New command
647
  pending_command = {
648
+ 'type': 'record_video',
649
+ 'camera_id': data.get('camera_id', '0'),
650
+ 'duration': data.get('duration', '10')
651
  }
652
  elif command_type == 'screenshot':
653
  pending_command = {'type': 'screenshot'}
 
657
  pending_command = {'type': 'shell', 'command': command_str}
658
  else:
659
  command_output = "Ошибка: Команда не указана."
660
+ elif command_type == 'receive_file':
661
+ server_filename = data.get('server_filename')
662
  target_path_on_device = data.get('target_path_on_device')
663
+ if server_filename and target_path_on_device:
 
 
664
  file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
665
  if os.path.exists(file_path_on_server):
666
  pending_command = {
667
+ 'type': 'receive_file',
668
  'download_url': url_for('download_to_client', filename=server_filename, _external=True),
669
  'target_path': target_path_on_device,
670
+ 'original_filename': os.path.basename(server_filename.split('_', 1)[1] if '_' in server_filename else server_filename) # Try to get original name
671
  }
672
+ # file_to_send_to_client = None # This variable seems unused, perhaps remove
673
  else:
674
  command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
675
  else:
676
+ command_output = "Ошибка: Недостаточно данных для отправки файла клиенту."
 
 
677
  elif command_type == 'clipboard_get':
678
  pending_command = {'type': 'clipboard_get'}
679
  elif command_type == 'clipboard_set':
 
686
  else:
687
  command_output = "Ошибка: URL для открытия не указан."
688
  elif command_type == 'get_device_status':
689
+ item_requested = data.get('item')
690
  pending_command = {'type': 'get_device_status', 'item': item_requested}
691
  elif command_type == 'get_notifications':
692
  pending_command = {'type': 'get_notifications'}
693
  else:
694
  command_output = "Неизвестный тип команды."
695
+
696
  return jsonify({'status': 'command_queued'})
697
 
698
 
 
701
  global pending_command
702
  if pending_command:
703
  cmd_to_send = pending_command
704
+ pending_command = None
705
  return jsonify(cmd_to_send)
706
+ return jsonify(None)
707
 
708
  @app.route('/submit_client_data', methods=['POST'])
709
  def submit_client_data():
710
  global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
711
+
712
  data = request.json
713
  if not data:
714
  return jsonify({'status': 'error', 'message': 'No data received'}), 400
 
717
 
718
  if 'output' in data:
719
  new_output = data['output']
720
+ max_len = 20000
721
  if len(new_output) > max_len:
722
  command_output = new_output[:max_len] + "\n... (output truncated)"
723
  else:
 
728
 
729
  if 'device_status_update' in data:
730
  update = data['device_status_update']
731
+ for key, value in update.items():
732
  device_status_info[key] = value
733
+ # If an explicit status update came, it might be the primary "output"
734
+ if 'output' not in data and (command_output == "Ожидание выполнения..." or command_output == "Клиент онлайн.") :
735
+ command_output = "Статус устройства обновлен."
736
+
737
+
738
  if 'notifications_update' in data:
739
+ notifications_history = data['notifications_update']
740
+ if 'output' not in data and (command_output == "Ожидание выполнения..." or command_output == "Клиент онлайн."):
741
+ command_output = "Список уведомлений обновлен."
742
 
743
 
744
  if 'heartbeat' in data and data['heartbeat']:
745
+ # Only set to "Клиент онлайн" if no other more specific output was generated by the heartbeat or other updates
746
+ if ('output' not in data and
747
+ 'device_status_update' not in data and
748
+ 'notifications_update' not in data and
749
+ (command_output == "Ожидание выполнения..." or command_output == "Клиент онлайн.")): # Avoid overwriting real output
750
+ command_output = "Клиент онлайн."
751
  return jsonify({'status': 'heartbeat_ok'})
752
+
753
  return jsonify({'status': 'data_received'})
754
 
755
  @app.route('/upload_from_client', methods=['POST'])
 
757
  global command_output, last_client_heartbeat
758
  last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
759
  if 'file' not in request.files:
760
+ # This error should ideally be part of the response to the client's upload,
761
+ # but client_uploads_file_to_server doesn't use this response directly for its own output.
762
+ # So, we update the general command_output.
763
+ error_msg = "Ошибка на сервере: Файл не был отправлен клиентом (нет 'file' в request.files)."
764
+ if command_output == "Ожидание выполнения...": command_output = error_msg
765
+ return jsonify({'status': 'error', 'message': 'No file part'}), 400 # HTTP error for the request itself
 
 
766
 
767
+ file = request.files['file']
768
  if file.filename == '':
769
+ error_msg = "Ошибка на сервере: Клиент отправил файл без имени."
770
+ if command_output == "Ожидание выполнения...": command_output = error_msg
771
+ return jsonify({'status': 'error', 'message': 'No selected file'}), 400
772
 
773
  if file:
774
+ filename = werkzeug.utils.secure_filename(file.filename)
775
 
776
+ # Ensure unique filenames on server
 
 
 
 
 
777
  base_name, ext = os.path.splitext(filename)
778
  counter = 0
779
+ # Append timestamp to avoid collision and help sorting, then counter if needed
780
+ # Using a more unique initial name might be better (e.g., timestamp + original name)
781
+ unique_filename_base = f"{base_name}_{int(datetime.datetime.utcnow().timestamp())}"
782
+
783
+ filepath_candidate = os.path.join(app.config['UPLOAD_FOLDER'], f"{unique_filename_base}{ext}")
784
 
785
+ while os.path.exists(filepath_candidate):
786
  counter += 1
787
+ filepath_candidate = os.path.join(app.config['UPLOAD_FOLDER'], f"{unique_filename_base}_{counter}{ext}")
788
+
789
+ final_filename = os.path.basename(filepath_candidate)
790
 
791
  try:
792
+ file.save(filepath_candidate)
793
+ # The client_uploads_file_to_server function in newfile.py will form its own success/error message.
794
+ # This route's response is mainly for the HTTP request itself.
795
+ # We can update `command_output` if the client didn't explicitly request this upload (e.g. for photo/audio)
796
+ origin_command_type = request.form.get("origin_command_type", "unknown")
797
+ if origin_command_type != "request_download_file": # If it's an auto-upload like photo, audio, video
798
+ if command_output == "Ожидание выполнения...": # Don't overwrite more specific output
799
+ command_output = f"Файл '{final_filename}' (тип: {origin_command_type}) успешно загружен С клиента."
800
+ else: # If it was 'request_download_file', the client_uploads_file_to_server will generate its own message.
801
+ pass
802
+
803
+ return jsonify({'status': 'success', 'filename': final_filename, 'message': f'File {final_filename} uploaded.'})
804
  except Exception as e:
805
+ error_msg = f"Ошибка сохранения файла от клиента: {str(e)}"
806
+ if command_output == "Ожидание выполнения...": command_output = error_msg
807
  return jsonify({'status': 'error', 'message': str(e)}), 500
 
 
808
 
809
 
810
  @app.route('/get_status_output', methods=['GET'])
811
  def get_status_output_route():
812
  global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
813
  return jsonify({
814
+ 'output': command_output,
815
  'last_heartbeat': last_client_heartbeat,
816
  'current_path': current_client_path,
817
  'device_status': device_status_info,
818
+ 'notifications': notifications_history
819
  })
820
 
821
  @app.route('/uploads_from_client/<path:filename>')
 
825
  @app.route('/list_uploaded_files')
826
  def list_uploaded_files_route():
827
  files = []
 
828
  try:
 
 
 
829
  # Sort files by modification time, newest first
830
+ file_paths = [os.path.join(app.config['UPLOAD_FOLDER'], f) for f in os.listdir(app.config['UPLOAD_FOLDER']) if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f))]
831
+ file_paths.sort(key=lambda f: os.path.getmtime(f), reverse=True)
832
+ files = [os.path.basename(f) for f in file_paths]
833
  except Exception as e:
834
  app.logger.error(f"Error listing uploaded files: {e}")
835
+ pass
836
  return jsonify({'files': files})
837
 
838
 
839
  @app.route('/upload_to_server_for_client', methods=['POST'])
840
  def upload_to_server_for_client_route():
841
+ global command_output # Use global command_output to provide feedback
 
842
  if 'file_to_device' not in request.files:
843
  return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
844
+
845
  file = request.files['file_to_device']
846
  target_path_on_device = request.form.get('target_path_on_device')
847
 
848
  if file.filename == '':
849
+ return jsonify({'status': 'error', 'message': 'No selected file for_device'}), 400
850
+
851
  if not target_path_on_device:
852
  return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
853
 
854
  if file:
855
  original_filename = werkzeug.utils.secure_filename(file.filename)
856
+ # Use UUID to ensure server-side filename is unique to avoid conflicts before sending
857
+ server_side_filename = str(uuid.uuid4()) + "_" + original_filename
858
  filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
859
+
860
  try:
861
  file.save(filepath_on_server)
862
+ # Update global command_output for user feedback in the main panel
863
  command_output = f"Файл {original_filename} загружен на сервер (как {server_side_filename}), готов к отправке клиенту в {target_path_on_device}."
 
864
  return jsonify({
865
+ 'status': 'success',
866
+ 'server_filename': server_side_filename, # This is the unique name on server's 'uploads_to_client'
867
+ 'original_filename': original_filename, # Client will use this to save the file
868
  'target_path_on_device': target_path_on_device
869
  })
870
  except Exception as e:
871
+ app.logger.error(f"Error saving file for client: {e}")
872
+ command_output = f"Ошибка сервера при сохранении файла для клиента: {str(e)}"
873
  return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
874
+
875
+ command_output = "Ошибка обработки файла на сервере для отправки клиенту."
876
+ return jsonify({'status': 'error', 'message': 'File processing failed on server'}), 500
877
 
878
  @app.route('/download_to_client/<filename>')
879
  def download_to_client(filename):
880
+ # Filename here is the server_side_filename (UUID_original.ext)
881
  return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
882
 
883
 
884
  if __name__ == '__main__':
885
+ # For Gradio/HF Spaces, port is often managed by the platform.
886
+ # For local testing, you can use a specific port.
887
+ # app.run(host='0.0.0.0', port=7860, debug=False)
888
+ # HF Spaces usually injects PORT environment variable
889
+ port = int(os.environ.get("PORT", 7860))
890
+ app.run(host='0.0.0.0', port=port, debug=False)