feat: docker-серверы - приложения в ~/apps под управлением бота

serverus: DockerHost - установка Docker и ingress Caddy, deploy приложения
с проверкой compose и caddy.conf до изменений и пересозданием только
изменившихся сервисов, status, control, logs, remove. nftables
перезапускается только при изменении конфига, Docker следом
восстанавливает свои правила; release восстанавливает конфиг Debian
вместо остановки nftables.

Telegram: прием документов и отправка файлов, раздача файлов своего
Bot API сервера через nginx (telegram-files).

App: общая модель Managed для Point и Platform, DockerApp и версии
файлов AppVersion, разбор архивов (uploads), сценарии платформы
(platforms), задачи и меню docker-серверов в боте.

Проверено на стенде Debian 13 с local_certs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
protokeyandClaude Opus 5.5 committed 2026-09-29 20:27:05 +04:00
1 parent 65c19ea1cd
commit a8d1f286d1
42 files changed
+3509 -232

No files matched your search

+45 -1
View File
@@ -8,11 +8,32 @@ CHAT_ID = 42
class FakeApi:
"""Подменяет TeleBot: запоминает отправленные и удаленные сообщения."""
"""Подменяет TeleBot: запоминает отправленные и удаленные сообщения, отдает присланные документы."""
def __init__(self):
self.sent = []
self.deleted = []
self.documents = []
self.files = {}
"""file_id -> содержимое присланного документа."""
def get_file(self, file_id):
from .api import WORK_DIR, uses_own_server
# свой сервер отдает путь на своем диске, облако - относительный
path = f'{WORK_DIR}/1:token/documents/{file_id}' if uses_own_server() else f'documents/{file_id}'
return SimpleNamespace(file_id=file_id, file_path=path, file_size=len(self.files[file_id]))
def download_file(self, file_path):
return self.files[file_path.rsplit('/', 1)[-1]]
def send_document(self, chat_id, document, caption=None, reply_markup=None, parse_mode=None):
content = document.file.read()
self.documents.append(SimpleNamespace(
chat_id=chat_id, file_name=document.file_name, content=content, caption=caption, reply_markup=reply_markup,
))
self.sent.append(SimpleNamespace(chat_id=chat_id, text=caption, reply_markup=reply_markup, parse_mode=parse_mode))
return SimpleNamespace(message_id=len(self.sent))
def send_message(self, chat_id, text, reply_markup=None, parse_mode=None):
self.sent.append(SimpleNamespace(chat_id=chat_id, text=text, reply_markup=reply_markup, parse_mode=parse_mode))
@@ -51,6 +72,19 @@ def message_update_json(update_id: int, text: str, chat_type: str = 'private', u
}
def document_update_json(
update_id: int, file_id: str, file_name: str, size: int, caption: str = '', user_id: int = USER_ID,
) -> dict:
"""Обновление с документом в том виде, в каком его присылает Bot API."""
update = message_update_json(update_id, '', user_id=user_id)
message = update['message']
del message['text']
message['document'] = {'file_id': file_id, 'file_unique_id': file_id, 'file_name': file_name, 'file_size': size}
if caption:
message['caption'] = caption
return update
class Conversation:
"""Диалог пользователя с ботом: say() отправляет сообщение и возвращает ответ бота."""
@@ -68,3 +102,13 @@ class Conversation:
sent_before = len(self.api.sent)
self._handle_update(self.api, self.bot, message_update(self._update_id, text, user_id=self.user_id))
return self.api.last_text if len(self.api.sent) > sent_before else None
def send_document(self, file_name: str, content: bytes, caption: str = '') -> str | None:
"""Присылает боту документ и возвращает ответ бота."""
self._update_id += 1
file_id = f'file{self._update_id}'
self.api.files[file_id] = content
sent_before = len(self.api.sent)
update = document_update_json(self._update_id, file_id, file_name, len(content), caption, self.user_id)
self._handle_update(self.api, self.bot, types.Update.de_json(update))
return self.api.last_text if len(self.api.sent) > sent_before else None