lockbot 2.9.2__tar.gz → 2.9.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lockbot-2.9.2/python/lockbot.egg-info → lockbot-2.9.4}/PKG-INFO +17 -2
- {lockbot-2.9.2 → lockbot-2.9.4}/README.md +16 -1
- {lockbot-2.9.2 → lockbot-2.9.4}/pyproject.toml +1 -1
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/admin/router.py +87 -63
- lockbot-2.9.4/python/lockbot/backend/app/backup/service.py +618 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/models.py +1 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/router.py +2 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/schemas.py +1 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/main.py +26 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/base_bot.py +9 -3
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/i18n/en.py +2 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/i18n/zh.py +2 -0
- {lockbot-2.9.2 → lockbot-2.9.4/python/lockbot.egg-info}/PKG-INFO +17 -2
- lockbot-2.9.2/python/lockbot/backend/app/backup/service.py +0 -191
- {lockbot-2.9.2 → lockbot-2.9.4}/LICENSE +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/MANIFEST.in +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/admin/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/audit/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/audit/models.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/audit/router.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/audit/service.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/auth/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/auth/dependencies.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/auth/models.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/auth/router.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/auth/schemas.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/backup/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/backup/router.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/backup/scheduler.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/encryption.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/manager.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/bots/webhook_handler.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/config.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/database.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/logs/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/rate_limit.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/settings/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/settings/models.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/backend/app/settings/router.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/bot_instance.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/config.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/device_bot.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/device_usage_alert.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/device_usage_utils.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/entry.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/env.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/handler.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/i18n/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/io.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/message_adapter.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/msg_utils.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/node_bot.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/platforms/__init__.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/platforms/infoflow.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/queue_bot.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/request.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/scheduler.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot/core/utils.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot.egg-info/SOURCES.txt +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot.egg-info/dependency_links.txt +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot.egg-info/requires.txt +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/python/lockbot.egg-info/top_level.txt +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/setup.cfg +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/tools/create_super_admin.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/tools/gen_keys.py +0 -0
- {lockbot-2.9.2 → lockbot-2.9.4}/tools/reset_super_admin_password.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lockbot
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.4
|
|
4
4
|
Summary: Cluster resource management bot for IM platforms
|
|
5
5
|
Author-email: Jianbang Yang <yangjianbang112@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -150,10 +150,25 @@ See `python/lockbot/core/config.py` for the full configuration reference.
|
|
|
150
150
|
|
|
151
151
|
## Development
|
|
152
152
|
|
|
153
|
+
### Dev Server
|
|
154
|
+
|
|
153
155
|
```bash
|
|
154
|
-
# Install
|
|
156
|
+
# Install dependencies
|
|
155
157
|
pip install -e ".[dev]"
|
|
158
|
+
cd frontend && npm install && cd ..
|
|
159
|
+
|
|
160
|
+
# Start backend (port 8002, hot-reload)
|
|
161
|
+
bash scripts/dev/backend.sh
|
|
162
|
+
|
|
163
|
+
# Start frontend (port 8001, proxies /api/* -> localhost:8002)
|
|
164
|
+
bash scripts/dev/frontend.sh
|
|
165
|
+
```
|
|
156
166
|
|
|
167
|
+
Both servers support hot-reload — code changes take effect automatically.
|
|
168
|
+
|
|
169
|
+
### Testing
|
|
170
|
+
|
|
171
|
+
```bash
|
|
157
172
|
# Run tests
|
|
158
173
|
pytest
|
|
159
174
|
|
|
@@ -107,10 +107,25 @@ See `python/lockbot/core/config.py` for the full configuration reference.
|
|
|
107
107
|
|
|
108
108
|
## Development
|
|
109
109
|
|
|
110
|
+
### Dev Server
|
|
111
|
+
|
|
110
112
|
```bash
|
|
111
|
-
# Install
|
|
113
|
+
# Install dependencies
|
|
112
114
|
pip install -e ".[dev]"
|
|
115
|
+
cd frontend && npm install && cd ..
|
|
116
|
+
|
|
117
|
+
# Start backend (port 8002, hot-reload)
|
|
118
|
+
bash scripts/dev/backend.sh
|
|
119
|
+
|
|
120
|
+
# Start frontend (port 8001, proxies /api/* -> localhost:8002)
|
|
121
|
+
bash scripts/dev/frontend.sh
|
|
122
|
+
```
|
|
113
123
|
|
|
124
|
+
Both servers support hot-reload — code changes take effect automatically.
|
|
125
|
+
|
|
126
|
+
### Testing
|
|
127
|
+
|
|
128
|
+
```bash
|
|
114
129
|
# Run tests
|
|
115
130
|
pytest
|
|
116
131
|
|
|
@@ -3,16 +3,10 @@ Admin API routes.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import contextlib
|
|
6
|
-
import io
|
|
7
|
-
import json
|
|
8
6
|
import os
|
|
9
|
-
import shutil
|
|
10
|
-
import tempfile
|
|
11
|
-
import zipfile
|
|
12
|
-
from datetime import datetime
|
|
13
7
|
|
|
14
|
-
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
15
|
-
from fastapi.responses import FileResponse
|
|
8
|
+
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
|
|
9
|
+
from fastapi.responses import FileResponse
|
|
16
10
|
from sqlalchemy.orm import Session
|
|
17
11
|
from starlette.background import BackgroundTask
|
|
18
12
|
|
|
@@ -300,78 +294,108 @@ def download_backup(
|
|
|
300
294
|
_admin: User = Depends(require_super_admin),
|
|
301
295
|
db: Session = Depends(get_db),
|
|
302
296
|
):
|
|
303
|
-
"""Download full
|
|
304
|
-
from lockbot.backend.app.
|
|
297
|
+
"""Download full backup archive (DB + bot states + manifest) as zip (super_admin only)."""
|
|
298
|
+
from lockbot.backend.app.backup.service import create_backup_archive
|
|
305
299
|
|
|
306
|
-
db_url = DATABASE_URL
|
|
307
|
-
if not db_url.startswith("sqlite:///"):
|
|
308
|
-
raise HTTPException(status_code=400, detail="Backup only supported for SQLite")
|
|
309
|
-
|
|
310
|
-
db_path = db_url[len("sqlite:///") :]
|
|
311
|
-
if not os.path.exists(db_path):
|
|
312
|
-
raise HTTPException(status_code=404, detail="Database file not found")
|
|
313
|
-
|
|
314
|
-
# Copy to temp file to avoid sending a mid-write database
|
|
315
|
-
fd, tmp_path = tempfile.mkstemp(suffix=".db")
|
|
316
|
-
os.close(fd)
|
|
317
300
|
try:
|
|
318
|
-
|
|
319
|
-
except
|
|
320
|
-
|
|
321
|
-
os.unlink(tmp_path)
|
|
322
|
-
raise
|
|
301
|
+
zip_path = create_backup_archive(password=None)
|
|
302
|
+
except RuntimeError as e:
|
|
303
|
+
raise HTTPException(status_code=400, detail=str(e)) from None
|
|
323
304
|
|
|
324
305
|
write_audit_log(db, _admin, "admin.backup", ip=request.client.host if request.client else None)
|
|
325
306
|
db.commit()
|
|
326
307
|
|
|
327
|
-
filename =
|
|
308
|
+
filename = zip_path.name
|
|
328
309
|
return FileResponse(
|
|
329
|
-
path=
|
|
310
|
+
path=str(zip_path),
|
|
330
311
|
filename=filename,
|
|
331
|
-
media_type="application/
|
|
332
|
-
background=BackgroundTask(
|
|
312
|
+
media_type="application/zip",
|
|
313
|
+
background=BackgroundTask(_cleanup_backup_tmp, zip_path),
|
|
333
314
|
)
|
|
334
315
|
|
|
335
316
|
|
|
336
|
-
|
|
317
|
+
def _cleanup_backup_tmp(zip_path):
|
|
337
318
|
|
|
319
|
+
with contextlib.suppress(OSError):
|
|
320
|
+
zip_path.unlink(missing_ok=True)
|
|
321
|
+
with contextlib.suppress(OSError):
|
|
322
|
+
zip_path.parent.rmdir()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@router.post("/backup/restore/preview")
|
|
326
|
+
async def preview_restore_backup(
|
|
327
|
+
file: UploadFile = File(...),
|
|
328
|
+
password: str | None = Form(default=None),
|
|
329
|
+
source_secret_key: str | None = Form(default=None),
|
|
330
|
+
_admin: User = Depends(require_super_admin),
|
|
331
|
+
db: Session = Depends(get_db),
|
|
332
|
+
):
|
|
333
|
+
"""Preview restoring bots from a unified backup zip (super_admin only)."""
|
|
334
|
+
from lockbot.backend.app.backup.service import restore_preview
|
|
338
335
|
|
|
339
|
-
|
|
340
|
-
|
|
336
|
+
content = await file.read()
|
|
337
|
+
if not content:
|
|
338
|
+
raise HTTPException(status_code=400, detail="Backup file is empty")
|
|
339
|
+
try:
|
|
340
|
+
return restore_preview(content, password or None, source_secret_key or None, _admin.id, db)
|
|
341
|
+
except ValueError as e:
|
|
342
|
+
err = str(e)
|
|
343
|
+
if err == "BACKUP_PASSWORD_REQUIRED":
|
|
344
|
+
raise HTTPException(status_code=422, detail="BACKUP_PASSWORD_REQUIRED") from None
|
|
345
|
+
if err == "BACKUP_PASSWORD_WRONG":
|
|
346
|
+
raise HTTPException(status_code=422, detail="BACKUP_PASSWORD_WRONG") from None
|
|
347
|
+
raise HTTPException(status_code=400, detail=err) from None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@router.post("/backup/restore/apply")
|
|
351
|
+
async def apply_restore_backup(
|
|
341
352
|
request: Request,
|
|
353
|
+
file: UploadFile = File(...),
|
|
354
|
+
password: str | None = Form(default=None),
|
|
355
|
+
source_secret_key: str | None = Form(default=None),
|
|
356
|
+
selected_uuids: str = Form(...),
|
|
342
357
|
_admin: User = Depends(require_super_admin),
|
|
343
358
|
db: Session = Depends(get_db),
|
|
344
359
|
):
|
|
345
|
-
"""
|
|
346
|
-
bots = db.query(Bot).filter(Bot.is_deleted.is_(False)).all()
|
|
360
|
+
"""Apply restore for selected bots from a backup zip (super_admin only).
|
|
347
361
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
# Determine data dir from config_overrides or default
|
|
352
|
-
data_dir = _DEFAULT_DATA_DIR
|
|
353
|
-
try:
|
|
354
|
-
overrides = json.loads(bot.config_overrides or "{}")
|
|
355
|
-
data_dir = overrides.get("DATA_DIR") or _DEFAULT_DATA_DIR
|
|
356
|
-
except (json.JSONDecodeError, AttributeError):
|
|
357
|
-
pass
|
|
358
|
-
state_file = os.path.join(data_dir, "bots", str(bot.id), "bot_state.json")
|
|
359
|
-
folder_name = f"bot_{bot.id}_{bot.name}"
|
|
360
|
-
if os.path.exists(state_file):
|
|
361
|
-
zf.write(state_file, f"{folder_name}/bot_state.json")
|
|
362
|
-
else:
|
|
363
|
-
# Create an empty placeholder so the admin knows the bot has no state
|
|
364
|
-
zf.writestr(f"{folder_name}/bot_state.json", "{}")
|
|
365
|
-
|
|
366
|
-
buf.seek(0)
|
|
367
|
-
content = buf.read()
|
|
368
|
-
|
|
369
|
-
write_audit_log(db, _admin, "admin.bot_states", ip=request.client.host if request.client else None)
|
|
370
|
-
db.commit()
|
|
362
|
+
selected_uuids: JSON array of bot UUIDs to restore, e.g. '["uuid1","uuid2"]'
|
|
363
|
+
"""
|
|
364
|
+
import json as _json
|
|
371
365
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
366
|
+
from lockbot.backend.app.backup.service import restore_apply
|
|
367
|
+
|
|
368
|
+
content = await file.read()
|
|
369
|
+
if not content:
|
|
370
|
+
raise HTTPException(status_code=400, detail="Backup file is empty")
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
uuids = _json.loads(selected_uuids)
|
|
374
|
+
except (ValueError, TypeError):
|
|
375
|
+
raise HTTPException(status_code=422, detail="selected_uuids must be a JSON array") from None
|
|
376
|
+
|
|
377
|
+
if not isinstance(uuids, list) or not uuids:
|
|
378
|
+
raise HTTPException(status_code=422, detail="selected_uuids must be a non-empty array")
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
result = restore_apply(content, password or None, source_secret_key or None, uuids, _admin.id, db)
|
|
382
|
+
except ValueError as e:
|
|
383
|
+
err = str(e)
|
|
384
|
+
if err == "BACKUP_PASSWORD_REQUIRED":
|
|
385
|
+
raise HTTPException(status_code=422, detail="BACKUP_PASSWORD_REQUIRED") from None
|
|
386
|
+
if err == "BACKUP_PASSWORD_WRONG":
|
|
387
|
+
raise HTTPException(status_code=422, detail="BACKUP_PASSWORD_WRONG") from None
|
|
388
|
+
raise HTTPException(status_code=400, detail=err) from None
|
|
389
|
+
|
|
390
|
+
write_audit_log(
|
|
391
|
+
db,
|
|
392
|
+
_admin,
|
|
393
|
+
"admin.restore",
|
|
394
|
+
detail={"created": result["created"], "overwritten": result["overwritten"]},
|
|
395
|
+
ip=request.client.host if request.client else None,
|
|
377
396
|
)
|
|
397
|
+
db.commit()
|
|
398
|
+
return result
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
_DEFAULT_DATA_DIR = os.environ.get("DATA_DIR", "/data")
|