feat: Tái cấu trúc bot sang kiến trúc cog, thêm hỗ trợ đa máy chủ, giới thiệu tính năng đăng ký bóng đá, giao diện web và quản lý cấu hình.

This commit is contained in:
2026-01-16 17:26:42 +07:00
parent 8c38357c28
commit b24365927a
39 changed files with 3864 additions and 997 deletions
+80
View File
@@ -0,0 +1,80 @@
from typing import List, Optional, Any
from sqlalchemy import select
from models.config import BotConfig
from infra.db import postgres
class ConfigRepository:
def __init__(self):
self.Session = postgres.get_sessionmaker()
async def get(self, guild_id: int, key: str, default: Any = None) -> Any:
"""Lấy giá trị config theo key"""
try:
async with self.Session() as session:
stmt = select(BotConfig).where(
BotConfig.guild_id == guild_id,
BotConfig.key == key
)
result = await session.execute(stmt)
config = result.scalar_one_or_none()
return config.value if config else default
except Exception as e:
print(f"Error getting config {key} for guild {guild_id}: {e}")
return default
async def set(self, guild_id: int, key: str, value: str, description: str = None) -> Optional[BotConfig]:
"""Cập nhật hoặc tạo mới config"""
try:
async with self.Session() as session:
stmt = select(BotConfig).where(
BotConfig.guild_id == guild_id,
BotConfig.key == key
)
result = await session.execute(stmt)
config = result.scalar_one_or_none()
if config:
config.value = str(value)
if description:
config.description = description
else:
config = BotConfig(guild_id=guild_id, key=key, value=str(value), description=description)
session.add(config)
await session.commit()
await session.refresh(config)
return config
except Exception as e:
print(f"Error setting config {key} for guild {guild_id}: {e}")
return None
async def get_all(self, guild_id: int) -> List[BotConfig]:
"""Lấy tất cả config"""
try:
async with self.Session() as session:
stmt = select(BotConfig).where(BotConfig.guild_id == guild_id)
result = await session.execute(stmt)
return list(result.scalars().all())
except Exception as e:
print(f"Error getting all configs for guild {guild_id}: {e}")
return []
async def delete(self, guild_id: int, key: str) -> bool:
"""Xóa config"""
try:
async with self.Session() as session:
stmt = select(BotConfig).where(
BotConfig.guild_id == guild_id,
BotConfig.key == key
)
result = await session.execute(stmt)
config = result.scalar_one_or_none()
if config:
await session.delete(config)
await session.commit()
return True
return False
except Exception as e:
print(f"Error deleting config {key} for guild {guild_id}: {e}")
return False
+56
View File
@@ -0,0 +1,56 @@
from typing import List, Optional
from sqlalchemy import select
from models.feature_toggle import FeatureToggle
from infra.db import postgres
class FeatureToggleRepository:
def __init__(self):
self.Session = postgres.get_sessionmaker()
async def get(self, guild_id: int, feature_name: str) -> bool:
"""Check if feature is enabled for guild"""
try:
async with self.Session() as session:
stmt = select(FeatureToggle).where(
FeatureToggle.guild_id == guild_id,
FeatureToggle.feature_name == feature_name
)
result = await session.execute(stmt)
toggle = result.scalar_one_or_none()
return toggle.is_enabled if toggle else False # Default Disabled
except Exception as e:
print(f"Error getting feature {feature_name} for guild {guild_id}: {e}")
return False
async def set(self, guild_id: int, feature_name: str, is_enabled: bool) -> Optional[FeatureToggle]:
try:
async with self.Session() as session:
stmt = select(FeatureToggle).where(
FeatureToggle.guild_id == guild_id,
FeatureToggle.feature_name == feature_name
)
result = await session.execute(stmt)
toggle = result.scalar_one_or_none()
if toggle:
toggle.is_enabled = is_enabled
else:
toggle = FeatureToggle(guild_id=guild_id, feature_name=feature_name, is_enabled=is_enabled)
session.add(toggle)
await session.commit()
await session.refresh(toggle)
return toggle
except Exception as e:
print(f"Error setting feature {feature_name} for guild {guild_id}: {e}")
return None
async def get_all_for_guild(self, guild_id: int) -> List[FeatureToggle]:
try:
async with self.Session() as session:
stmt = select(FeatureToggle).where(FeatureToggle.guild_id == guild_id)
result = await session.execute(stmt)
return list(result.scalars().all())
except Exception as e:
print(f"Error getting all features for guild {guild_id}: {e}")
return []
+67
View File
@@ -0,0 +1,67 @@
from typing import List, Optional
from sqlalchemy import select, delete
from models.football import FootballSubscription
from infra.db import postgres
class FootballRepository:
def __init__(self):
self.Session = postgres.get_sessionmaker()
async def add_subscription(self, guild_id: int, channel_id: int, team_name: str, team_id: int = None) -> bool:
try:
async with self.Session() as session:
# Check exist
stmt = select(FootballSubscription).where(
FootballSubscription.guild_id == guild_id,
FootballSubscription.team_name == team_name
)
existing = await session.execute(stmt)
if existing.scalar_one_or_none():
return False
sub = FootballSubscription(
guild_id=guild_id,
channel_id=channel_id,
team_name=team_name,
team_id=team_id
)
session.add(sub)
await session.commit()
return True
except Exception as e:
print(f"Error adding subscription: {e}")
return False
async def remove_subscription(self, guild_id: int, team_name: str) -> bool:
try:
async with self.Session() as session:
stmt = delete(FootballSubscription).where(
FootballSubscription.guild_id == guild_id,
FootballSubscription.team_name == team_name
)
result = await session.execute(stmt)
await session.commit()
return result.rowcount > 0
except Exception as e:
print(f"Error removing subscription: {e}")
return False
async def get_all_subscriptions(self) -> List[FootballSubscription]:
try:
async with self.Session() as session:
stmt = select(FootballSubscription)
result = await session.execute(stmt)
return list(result.scalars().all())
except Exception as e:
print(f"Error getting subscriptions: {e}")
return []
async def get_guild_subscriptions(self, guild_id: int) -> List[FootballSubscription]:
try:
async with self.Session() as session:
stmt = select(FootballSubscription).where(FootballSubscription.guild_id == guild_id)
result = await session.execute(stmt)
return list(result.scalars().all())
except Exception as e:
print(f"Error getting guild subscriptions: {e}")
return []
+49
View File
@@ -0,0 +1,49 @@
from typing import List, Optional
from sqlalchemy import select
from models.guild import Guild
from infra.db import postgres
class GuildRepository:
def __init__(self):
self.Session = postgres.get_sessionmaker()
async def get(self, guild_id: int) -> Optional[Guild]:
try:
async with self.Session() as session:
stmt = select(Guild).where(Guild.id == guild_id)
result = await session.execute(stmt)
return result.scalar_one_or_none()
except Exception as e:
print(f"Error getting guild {guild_id}: {e}")
return None
async def create_or_update(self, guild_id: int, name: str) -> Optional[Guild]:
try:
async with self.Session() as session:
stmt = select(Guild).where(Guild.id == guild_id)
result = await session.execute(stmt)
guild = result.scalar_one_or_none()
if guild:
guild.name = name
guild.is_active = True
else:
guild = Guild(id=guild_id, name=name, is_active=True)
session.add(guild)
await session.commit()
await session.refresh(guild)
return guild
except Exception as e:
print(f"Error upserting guild {guild_id}: {e}")
return None
async def get_all(self) -> List[Guild]:
try:
async with self.Session() as session:
stmt = select(Guild).where(Guild.is_active == True)
result = await session.execute(stmt)
return list(result.scalars().all())
except Exception as e:
print(f"Error getting all guilds: {e}")
return []
+15 -9
View File
@@ -9,33 +9,39 @@ class HomeDebtRepository:
def __init__(self):
self.Session = postgres.get_sessionmaker()
async def get(self, discord_user_id: int) -> Optional[HomeDebt]:
async def get(self, guild_id: int, discord_user_id: int) -> Optional[HomeDebt]:
"""Lấy thông tin thành viên"""
try:
async with self.Session() as session:
stmt = select(HomeDebt).where(HomeDebt.user_id == discord_user_id)
stmt = select(HomeDebt).where(
HomeDebt.guild_id == guild_id,
HomeDebt.user_id == discord_user_id
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
except Exception as e:
print(f"Error getting user: {e}")
return None
async def get_other(self, discord_user_id: int) -> Optional[HomeDebt]:
async def get_other(self, guild_id: int, discord_user_id: int) -> Optional[HomeDebt]:
"""Lấy thông tin thành viên khác"""
try:
async with self.Session() as session:
stmt = select(HomeDebt).where(HomeDebt.user_id != discord_user_id).limit(1)
stmt = select(HomeDebt).where(
HomeDebt.guild_id == guild_id,
HomeDebt.user_id != discord_user_id
).limit(1)
result = await session.execute(stmt)
return result.scalar_one_or_none()
except Exception as e:
print(f"Error getting other member: {e}")
return None
async def create_home_debt(self, user_id: int, value: int) -> Optional[HomeDebt]:
async def create_home_debt(self, guild_id: int, user_id: int, value: int) -> Optional[HomeDebt]:
"""Tạo mới khoản nợ"""
try:
async with self.Session() as session:
home_debt = HomeDebt(user_id=user_id, value=value)
home_debt = HomeDebt(guild_id=guild_id, user_id=user_id, value=value)
session.add(home_debt)
await session.commit()
await session.refresh(home_debt)
@@ -57,13 +63,13 @@ class HomeDebtRepository:
print(f"Error updating home debt: {e}")
return None
async def get_all(self) -> List[HomeDebt]:
async def get_all(self, guild_id: int) -> List[HomeDebt]:
"""Lấy tất cả khoản nợ"""
try:
async with self.Session() as session:
stmt = select(HomeDebt)
stmt = select(HomeDebt).where(HomeDebt.guild_id == guild_id)
result = await session.execute(stmt)
return result.scalars().all()
return list(result.scalars().all())
except Exception as e:
print(f"Error getting all home debts: {e}")
return []
+22 -15
View File
@@ -11,22 +11,25 @@ class ScoreRepository:
def __init__(self):
self.Session = postgres.get_sessionmaker()
async def get(self, user_id: int) -> Optional[Score]:
async def get(self, guild_id: int, user_id: int) -> Optional[Score]:
"""Lấy thông tin thành viên"""
try:
async with self.Session() as session:
stmt = select(Score).where(Score.user_id == user_id)
stmt = select(Score).where(
Score.guild_id == guild_id,
Score.user_id == user_id
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
except Exception as e:
print(f"Error getting user: {e}")
return None
async def create(self, user_id: int, point: int) -> Optional[Score]:
async def create(self, guild_id: int, user_id: int, point: int) -> Optional[Score]:
"""Tạo thông tin thành viên"""
try:
async with self.Session() as session:
currency = Score(user_id=user_id, point=point)
currency = Score(guild_id=guild_id, user_id=user_id, point=point)
session.add(currency)
await session.commit()
await session.refresh(currency)
@@ -35,13 +38,16 @@ class ScoreRepository:
print(f"Error creating user: {e}")
return None
async def update(self, user_id: int, point: int) -> Optional[Score]:
async def update(self, guild_id: int, user_id: int, point: int) -> Optional[Score]:
"""Cập nhật thông tin thành viên"""
try:
async with self.Session() as session:
stmt = (
update(Score)
.where(Score.user_id == user_id)
.where(
Score.guild_id == guild_id,
Score.user_id == user_id
)
.values(point=point)
.returning(Score)
)
@@ -52,22 +58,22 @@ class ScoreRepository:
print(f"Error updating user: {e}")
return None
async def get_all(self) -> List[Score]:
async def get_all(self, guild_id: int) -> List[Score]:
"""Lấy tất cả thông tin thành viên"""
try:
async with self.Session() as session:
stmt = select(Score).order_by(Score.point.desc())
stmt = select(Score).where(Score.guild_id == guild_id).order_by(Score.point.desc())
result = await session.execute(stmt)
return result.scalars().all()
except Exception as e:
print(f"Error getting all users: {e}")
return []
async def get_all_with_point(self) -> List[Score]:
async def get_all_with_point(self, guild_id: int) -> List[Score]:
"""Lấy tất cả thông tin thành viên và số dư"""
try:
async with self.Session() as session:
stmt = select(Score)
stmt = select(Score).where(Score.guild_id == guild_id)
result = await session.execute(stmt)
return result.scalars().all()
except Exception as e:
@@ -75,28 +81,29 @@ class ScoreRepository:
return []
# Get all with sort by point
async def get_all_with_sort_by_point(self) -> List[Score]:
async def get_all_with_sort_by_point(self, guild_id: int) -> List[Score]:
"""Lấy tất cả thông tin thành viên và sắp xếp theo số dư"""
try:
async with self.Session() as session:
stmt = select(Score).order_by(Score.point.desc())
stmt = select(Score).where(Score.guild_id == guild_id).order_by(Score.point.desc())
result = await session.execute(stmt)
return result.scalars().all()
except Exception as e:
print(f"Error getting all users with sort by point: {e}")
return []
async def upsert_or_increment_point(self, user_id: str, user_name: str, amount: int) -> Optional[int]:
async def upsert_or_increment_point(self, guild_id: int, user_id: str, user_name: str, amount: int) -> Optional[int]:
try:
async with self.Session() as session:
# Sử dụng PostgreSQL UPSERT (ON CONFLICT)
stmt = pg_insert(Score).values(
guild_id=int(guild_id),
user_id=int(user_id),
user_name=user_name,
user_name=str(user_name),
point=amount
)
stmt = stmt.on_conflict_do_update(
index_elements=['user_id'],
index_elements=['guild_id', 'user_id'], # Must match UniqueConstraint/Index
set_=dict(
point=Score.point + amount,
user_name=stmt.excluded.user_name