-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatabase.py
More file actions
628 lines (529 loc) · 17.9 KB
/
Copy pathdatabase.py
File metadata and controls
628 lines (529 loc) · 17.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
"""
Database operations layer - async CRUD for all models.
Provides high-level async interface for the application.
"""
from __future__ import annotations
import json
from datetime import datetime
from typing import Optional, List, Dict, Any, Tuple
from sqlalchemy import select, func, delete, update, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from models import (
Base,
User,
Bot,
EnvVar,
BotProcessState,
Deployment,
AuditLog,
BackupRecord,
SystemConfig,
init_db,
get_async_db,
upsert_bot_process_state,
get_bot_process_state,
get_running_bots,
update_bot_status,
create_audit_log,
get_bot_with_relations,
)
# Re-export for backward compatibility
__all__ = [
"Base",
"User",
"Bot",
"EnvVar",
"BotProcessState",
"Deployment",
"AuditLog",
"BackupRecord",
"SystemConfig",
"init_db",
"get_async_db",
# High-level operations
"get_user",
"get_or_create_user",
"update_user",
"ban_user",
"get_user_bots",
"count_user_bots",
"get_bot",
"get_bot_full",
"insert_bot",
"insert_bot_if_room",
"update_bot",
"delete_bot",
"set_bot_files",
"set_bot_status",
"set_env_var",
"get_env_vars",
"list_env_vars",
"delete_env_var",
"set_max_memory",
"set_restart_interval",
"set_auto_restart",
"toggle_auto_restart",
"get_all_bots",
"global_stats",
"get_users_page",
"get_audit_log_page",
"log_admin_action",
"get_system_config",
"set_system_config",
# Process state
"save_process_state",
"get_process_state",
# Deployments
"create_deployment",
"update_deployment",
"get_deployments",
# Backups
"create_backup_record",
"update_backup_record",
"get_backup_records",
]
# --- User operations ---
async def get_user(user_id: int) -> Optional[User]:
"""Get user by Telegram ID."""
async with get_async_db() as db:
return await db.get(User, user_id)
async def get_or_create_user(
user_id: int,
username: Optional[str] = None,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
language_code: Optional[str] = None,
) -> User:
"""Get existing user or create new one."""
async with get_async_db() as db:
user = await db.get(User, user_id)
if user:
# Update profile info
if username is not None:
user.username = username
if first_name is not None:
user.first_name = first_name
if last_name is not None:
user.last_name = last_name
if language_code is not None:
user.language_code = language_code
user.updated_at = datetime.utcnow()
else:
user = User(
id=user_id,
username=username,
first_name=first_name,
last_name=last_name,
language_code=language_code,
)
db.add(user)
await db.commit()
await db.refresh(user)
return user
async def update_user(user_id: int, **kwargs) -> Optional[User]:
"""Update user fields."""
async with get_async_db() as db:
user = await db.get(User, user_id)
if user:
for key, value in kwargs.items():
if hasattr(user, key):
setattr(user, key, value)
user.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(user)
return user
async def ban_user(user_id: int, banned: bool = True) -> bool:
"""Ban or unban a user."""
async with get_async_db() as db:
user = await db.get(User, user_id)
if user:
user.is_banned = banned
user.updated_at = datetime.utcnow()
await db.commit()
return True
return False
# --- Bot operations ---
async def get_user_bots(user_id: int) -> List[Bot]:
"""Get all bots owned by a user."""
async with get_async_db() as db:
result = await db.execute(
select(Bot)
.where(Bot.owner_id == user_id)
.order_by(Bot.created_at.desc())
)
return list(result.scalars().all())
async def count_user_bots(user_id: int) -> int:
"""Count bots owned by a user."""
async with get_async_db() as db:
result = await db.execute(
select(func.count(Bot.id)).where(Bot.owner_id == user_id)
)
return result.scalar() or 0
async def get_bot(bot_id: int) -> Optional[Bot]:
"""Get bot by ID."""
async with get_async_db() as db:
return await db.get(Bot, bot_id)
async def get_bot_full(bot_id: int) -> Optional[Bot]:
"""Get bot with all relationships."""
return await get_bot_with_relations(bot_id)
async def insert_bot(
owner_id: int,
name: str,
folder: str = "",
entry_file: str = "",
max_memory_mb: Optional[int] = None,
) -> Bot:
"""Create a new bot."""
async with get_async_db() as db:
bot = Bot(
owner_id=owner_id,
name=name,
folder=folder,
entry_file=entry_file,
max_memory_mb=max_memory_mb,
status="stopped",
)
db.add(bot)
await db.commit()
await db.refresh(bot)
return bot
async def insert_bot_if_room(
owner_id: int,
name: str,
max_bots: int,
max_memory_mb: Optional[int] = None,
) -> Optional[int]:
"""Create a new bot if user hasn't reached their limit. Returns bot_id or None."""
count = await count_user_bots(owner_id)
if count >= max_bots:
return None
bot = await insert_bot(owner_id, name, max_memory_mb=max_memory_mb)
return bot.id
async def update_bot(bot_id: int, **kwargs) -> Optional[Bot]:
"""Update bot fields."""
async with get_async_db() as db:
bot = await db.get(Bot, bot_id)
if bot:
for key, value in kwargs.items():
if hasattr(bot, key):
setattr(bot, key, value)
bot.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(bot)
return bot
async def delete_bot(bot_id: int) -> bool:
"""Delete a bot and all related data."""
async with get_async_db() as db:
bot = await db.get(Bot, bot_id)
if bot:
await db.delete(bot)
await db.commit()
return True
return False
async def set_bot_files(bot_id: int, folder: str, entry_file: str) -> bool:
"""Update bot folder and entry file."""
return await update_bot(bot_id, folder=folder, entry_file=entry_file) is not None
async def set_bot_status(bot_id: int, status: str, error: Optional[str] = None) -> None:
"""Update bot status."""
await update_bot_status(bot_id, status, error)
# --- Environment variables ---
async def set_env_var(bot_id: int, key: str, value: str) -> EnvVar:
"""Set or update an environment variable for a bot (value will be encrypted)."""
from env_crypto import env_crypto
encrypted_value = env_crypto.encrypt_value(value)
display_value = env_crypto.mask_value(value)
async with get_async_db() as db:
# Check if exists
result = await db.execute(
select(EnvVar).where(and_(EnvVar.bot_id == bot_id, EnvVar.key == key))
)
env_var = result.scalar_one_or_none()
if env_var:
env_var.value_encrypted = encrypted_value
env_var.display_value = display_value
else:
env_var = EnvVar(
bot_id=bot_id,
key=key,
value_encrypted=encrypted_value,
display_value=display_value,
)
db.add(env_var)
await db.commit()
await db.refresh(env_var)
return env_var
async def get_env_vars(bot_id: int) -> Dict[str, str]:
"""Get all environment variables for a bot (decrypted)."""
from env_crypto import env_crypto
async with get_async_db() as db:
result = await db.execute(
select(EnvVar).where(EnvVar.bot_id == bot_id)
)
env_vars = result.scalars().all()
return {
ev.key: env_crypto.decrypt_value(ev.value_encrypted)
for ev in env_vars
}
async def list_env_vars(bot_id: int) -> List[Dict[str, Any]]:
"""Get environment variables with masked display values."""
async with get_async_db() as db:
result = await db.execute(
select(EnvVar).where(EnvVar.bot_id == bot_id)
)
env_vars = result.scalars().all()
return [
{
"key": ev.key,
"display_value": ev.display_value,
}
for ev in env_vars
]
async def delete_env_var(bot_id: int, key: str) -> bool:
"""Delete an environment variable."""
async with get_async_db() as db:
result = await db.execute(
select(EnvVar).where(and_(EnvVar.bot_id == bot_id, EnvVar.key == key))
)
env_var = result.scalar_one_or_none()
if env_var:
await db.delete(env_var)
await db.commit()
return True
return False
# --- Bot settings ---
async def set_max_memory(bot_id: int, memory_mb: Optional[int]) -> bool:
"""Set max memory for a bot."""
bot = await update_bot(bot_id, max_memory_mb=memory_mb)
return bot is not None
async def set_restart_interval(bot_id: int, hours: int) -> bool:
"""Set restart interval for a bot."""
bot = await update_bot(bot_id, restart_interval_hours=hours)
return bot is not None
async def set_auto_restart(bot_id: int, enabled: bool) -> bool:
"""Set auto restart for a bot."""
bot = await update_bot(bot_id, auto_restart=enabled)
return bot is not None
async def toggle_auto_restart(bot_id: int) -> bool:
"""Toggle auto restart for a bot."""
async with get_async_db() as db:
bot = await db.get(Bot, bot_id)
if bot:
bot.auto_restart = not bot.auto_restart
bot.updated_at = datetime.utcnow()
await db.commit()
return bot.auto_restart
return False
# --- Admin operations ---
async def get_all_bots() -> List[Bot]:
"""Get all bots (admin)."""
async with get_async_db() as db:
result = await db.execute(
select(Bot).order_by(Bot.created_at.desc())
)
return list(result.scalars().all())
async def global_stats() -> Dict[str, int]:
"""Get global platform statistics."""
async with get_async_db() as db:
total_users = await db.execute(select(func.count(User.id)))
total_bots = await db.execute(select(func.count(Bot.id)))
running_bots = await db.execute(
select(func.count(Bot.id)).where(Bot.status == "running")
)
crashed_bots = await db.execute(
select(func.count(Bot.id)).where(Bot.status == "crashed")
)
return {
"total_users": total_users.scalar() or 0,
"total_bots": total_bots.scalar() or 0,
"running": running_bots.scalar() or 0,
"crashed": crashed_bots.scalar() or 0,
}
async def get_users_page(page: int, per_page: int) -> Tuple[List[User], int]:
"""Get paginated users."""
async with get_async_db() as db:
total = await db.execute(select(func.count(User.id)))
total_count = total.scalar() or 0
offset = (page - 1) * per_page
result = await db.execute(
select(User)
.order_by(User.created_at.desc())
.offset(offset)
.limit(per_page)
)
users = list(result.scalars().all())
return users, total_count
async def get_audit_log_page(page: int, per_page: int) -> Tuple[List[AuditLog], int]:
"""Get paginated audit logs."""
async with get_async_db() as db:
total = await db.execute(select(func.count(AuditLog.id)))
total_count = total.scalar() or 0
offset = (page - 1) * per_page
result = await db.execute(
select(AuditLog)
.order_by(AuditLog.created_at.desc())
.offset(offset)
.limit(per_page)
)
logs = list(result.scalars().all())
return logs, total_count
async def log_admin_action(
admin_id: int,
action: str,
target: Optional[str] = None,
details: Optional[str] = None,
) -> AuditLog:
"""Log an administrative action."""
return await create_audit_log(
admin_id=admin_id,
action=action,
target=target,
details=details,
)
# --- System config ---
async def get_system_config(key: str) -> Optional[str]:
"""Get system configuration value."""
async with get_async_db() as db:
result = await db.execute(
select(SystemConfig).where(SystemConfig.key == key)
)
config = result.scalar_one_or_none()
return config.value if config else None
async def set_system_config(key: str, value: str, description: str = "", is_secret: bool = False) -> SystemConfig:
"""Set system configuration value."""
async with get_async_db() as db:
result = await db.execute(
select(SystemConfig).where(SystemConfig.key == key)
)
config = result.scalar_one_or_none()
if config:
config.value = value
config.description = description
config.is_secret = is_secret
config.updated_at = datetime.utcnow()
else:
config = SystemConfig(
key=key,
value=value,
description=description,
is_secret=is_secret,
)
db.add(config)
await db.commit()
await db.refresh(config)
return config
# --- Process state ---
async def save_process_state(bot_id: int, state: Dict[str, Any]) -> BotProcessState:
"""Save bot process state to database."""
return await upsert_bot_process_state(bot_id, state)
async def get_process_state(bot_id: int) -> Optional[BotProcessState]:
"""Get bot process state from database."""
return await get_bot_process_state(bot_id)
# --- Deployments ---
async def create_deployment(
bot_id: int,
version: Optional[str] = None,
deployed_by: Optional[int] = None,
) -> Deployment:
"""Create a new deployment record."""
async with get_async_db() as db:
deployment = Deployment(
bot_id=bot_id,
version=version,
deployed_by=deployed_by,
status="pending",
started_at=datetime.utcnow(),
)
db.add(deployment)
await db.commit()
await db.refresh(deployment)
return deployment
async def update_deployment(
deployment_id: int,
status: Optional[str] = None,
image_tag: Optional[str] = None,
build_logs: Optional[str] = None,
) -> Optional[Deployment]:
"""Update deployment record."""
async with get_async_db() as db:
deployment = await db.get(Deployment, deployment_id)
if deployment:
if status is not None:
deployment.status = status
if image_tag is not None:
deployment.image_tag = image_tag
if build_logs is not None:
deployment.build_logs = build_logs
if status in ("success", "failed"):
deployment.completed_at = datetime.utcnow()
deployment.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(deployment)
return deployment
async def get_deployments(bot_id: int, limit: int = 50) -> List[Deployment]:
"""Get deployments for a bot."""
async with get_async_db() as db:
result = await db.execute(
select(Deployment)
.where(Deployment.bot_id == bot_id)
.order_by(Deployment.created_at.desc())
.limit(limit)
)
return list(result.scalars().all())
# --- Backups ---
async def create_backup_record(
backup_type: str,
file_path: Optional[str] = None,
) -> BackupRecord:
"""Create a new backup record."""
async with get_async_db() as db:
record = BackupRecord(
backup_type=backup_type,
status="pending",
file_path=file_path,
started_at=datetime.utcnow(),
)
db.add(record)
await db.commit()
await db.refresh(record)
return record
async def update_backup_record(
backup_id: int,
status: Optional[str] = None,
file_path: Optional[str] = None,
file_size_mb: Optional[int] = None,
database_size_mb: Optional[int] = None,
bots_included: Optional[int] = None,
error_message: Optional[str] = None,
) -> Optional[BackupRecord]:
"""Update backup record."""
async with get_async_db() as db:
record = await db.get(BackupRecord, backup_id)
if record:
if status is not None:
record.status = status
if file_path is not None:
record.file_path = file_path
if file_size_mb is not None:
record.file_size_mb = file_size_mb
if database_size_mb is not None:
record.database_size_mb = database_size_mb
if bots_included is not None:
record.bots_included = bots_included
if error_message is not None:
record.error_message = error_message
if status in ("success", "failed"):
record.completed_at = datetime.utcnow()
record.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(record)
return record
async def get_backup_records(limit: int = 50) -> List[BackupRecord]:
"""Get backup history."""
async with get_async_db() as db:
result = await db.execute(
select(BackupRecord)
.order_by(BackupRecord.created_at.desc())
.limit(limit)
)
return list(result.scalars().all())