First commit
This commit is contained in:
286
bot/handlers/custom/econom.py
Normal file
286
bot/handlers/custom/econom.py
Normal file
@@ -0,0 +1,286 @@
|
||||
# modules/economy.py
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple, List
|
||||
|
||||
import aiofiles
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command, CommandObject
|
||||
from aiogram.types import Message, User
|
||||
from aiogram.utils.markdown import hbold
|
||||
|
||||
from bot.filters import IsOwner
|
||||
|
||||
# ==================== Конфигурация ====================
|
||||
ECONOMY_FILE = Path("data/economy.json")
|
||||
ECONOMY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
CURRENCY_NAME = "коинов"
|
||||
|
||||
|
||||
# ==================== Хранилище ====================
|
||||
class Economy:
|
||||
def __init__(self):
|
||||
self.data: Dict[int, dict] = {} # user_id → {balance, username, full_name}
|
||||
self.username_to_id: Dict[str, int] = {} # username.lower() → user_id
|
||||
|
||||
async def load(self):
|
||||
if not ECONOMY_FILE.exists():
|
||||
return
|
||||
try:
|
||||
async with aiofiles.open(ECONOMY_FILE, "r", encoding="utf-8") as f:
|
||||
content = await f.read()
|
||||
if not content.strip():
|
||||
return
|
||||
raw = json.loads(content)
|
||||
self.data = {int(uid): info for uid, info in raw.items()}
|
||||
self.username_to_id = {}
|
||||
for uid, info in self.data.items():
|
||||
username = info.get("username")
|
||||
if username:
|
||||
self.username_to_id[username.lower()] = uid
|
||||
except Exception as e:
|
||||
print(f"[Economy] Load error: {e}")
|
||||
|
||||
async def save(self):
|
||||
try:
|
||||
async with aiofiles.open(ECONOMY_FILE, "w", encoding="utf-8") as f:
|
||||
await f.write(json.dumps(self.data, indent=2, ensure_ascii=False))
|
||||
except Exception as e:
|
||||
print(f"[Economy] Save error: {e}")
|
||||
|
||||
async def ensure_user(self, user_id: int, username: Optional[str] = None, full_name: Optional[str] = None):
|
||||
"""Создаёт пользователя с 0 балансом, если его нет"""
|
||||
if user_id not in self.data:
|
||||
self.data[user_id] = {
|
||||
"balance": 50,
|
||||
"username": username,
|
||||
"full_name": full_name or "Unknown User"
|
||||
}
|
||||
if username:
|
||||
self.username_to_id[username.lower()] = user_id
|
||||
await self.save()
|
||||
|
||||
# Обновляем данные, если изменились
|
||||
updated = False
|
||||
if username and self.data[user_id]["username"] != username:
|
||||
old = self.data[user_id]["username"]
|
||||
if old and old.lower() in self.username_to_id:
|
||||
del self.username_to_id[old.lower()]
|
||||
self.data[user_id]["username"] = username
|
||||
self.username_to_id[username.lower()] = user_id
|
||||
updated = True
|
||||
|
||||
if full_name and self.data[user_id]["full_name"] != full_name:
|
||||
self.data[user_id]["full_name"] = full_name
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
await self.save()
|
||||
|
||||
async def get_balance(self, user: User) -> int:
|
||||
await self.ensure_user(user.id, user.username, user.full_name)
|
||||
return self.data[user.id]["balance"]
|
||||
|
||||
async def modify_balance(self, user_id: int, delta: int, username: Optional[str] = None, full_name: Optional[str] = None) -> int:
|
||||
await self.ensure_user(user_id, username, full_name)
|
||||
self.data[user_id]["balance"] += delta
|
||||
await self.save()
|
||||
return self.data[user_id]["balance"]
|
||||
|
||||
async def set_balance(self, user_id: int, amount: int, username: Optional[str] = None, full_name: Optional[str] = None):
|
||||
await self.ensure_user(user_id, username, full_name)
|
||||
self.data[user_id]["balance"] = amount
|
||||
await self.save()
|
||||
|
||||
async def delete_user(self, user_id: int) -> bool:
|
||||
if user_id in self.data:
|
||||
username = self.data[user_id].get("username")
|
||||
if username and username.lower() in self.username_to_id:
|
||||
del self.username_to_id[username.lower()]
|
||||
del self.data[user_id]
|
||||
await self.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def resolve_id(self, username: str) -> Optional[int]:
|
||||
return self.username_to_id.get(username.removeprefix("@").lower())
|
||||
|
||||
def get_top(self, limit: int = 20) -> List[Tuple[int, int, str, str]]:
|
||||
"""Топ только с положительным балансом"""
|
||||
items = []
|
||||
for uid, info in self.data.items():
|
||||
bal = info["balance"]
|
||||
if bal <= -1000:
|
||||
continue # ← НЕ ПОКАЗЫВАЕМ НУЛЕВЫЕ БАЛАНСЫ
|
||||
items.append((
|
||||
uid,
|
||||
bal,
|
||||
info.get("username") or "",
|
||||
info.get("full_name") or f"User#{uid}"
|
||||
))
|
||||
return sorted(items, key=lambda x: x[1], reverse=True)[:limit]
|
||||
|
||||
|
||||
economy = Economy()
|
||||
router = Router(name="economy")
|
||||
|
||||
|
||||
# ==================== Утилиты ====================
|
||||
def fmt(num: int) -> str:
|
||||
return f"{num:,}".replace(",", " ")
|
||||
|
||||
|
||||
def user_mention(user: Optional[User] = None, username: str = "", full_name: str = "") -> str:
|
||||
if user:
|
||||
if user.username:
|
||||
return f"@{user.username}"
|
||||
return hbold(user.full_name or "Unknown")
|
||||
if username:
|
||||
return f"@{username}"
|
||||
return hbold(full_name or "Unknown User")
|
||||
|
||||
|
||||
# ==================== Функция для регистрации при любом сообщении ====================
|
||||
async def register_user_on_message(message: Message):
|
||||
"""Вызывай эту функцию в глобальном обработчике сообщений"""
|
||||
if message.from_user:
|
||||
await economy.ensure_user(
|
||||
user_id=message.from_user.id,
|
||||
username=message.from_user.username,
|
||||
full_name=message.from_user.full_name
|
||||
)
|
||||
|
||||
|
||||
# ==================== Команды ====================
|
||||
|
||||
@router.message(Command("balance"))
|
||||
async def cmd_balance(message: Message, command: CommandObject):
|
||||
target = message.from_user
|
||||
|
||||
if message.reply_to_message and message.reply_to_message.from_user:
|
||||
target = message.reply_to_message.from_user
|
||||
elif command.args:
|
||||
uid = economy.resolve_id(command.args.strip())
|
||||
if uid:
|
||||
info = economy.data[uid]
|
||||
name = user_mention(username=info.get("username"), full_name=info.get("full_name"))
|
||||
await message.answer(f"Баланс {name}: {hbold(fmt(info['balance']))} {CURRENCY_NAME}")
|
||||
return
|
||||
|
||||
bal = await economy.get_balance(target)
|
||||
await message.answer(f"Баланс {user_mention(target)}: {hbold(fmt(bal))} {CURRENCY_NAME}")
|
||||
|
||||
|
||||
async def _get_target(message: Message, arg: Optional[str] = None):
|
||||
if message.reply_to_message and message.reply_to_message.from_user:
|
||||
return message.reply_to_message.from_user, None, None
|
||||
|
||||
if arg:
|
||||
username_raw = arg.strip().removeprefix("@")
|
||||
uid = economy.resolve_id("@" + username_raw) or economy.resolve_id(username_raw)
|
||||
return None, uid, username_raw
|
||||
|
||||
return message.from_user, None, None
|
||||
|
||||
|
||||
@router.message(Command("setbalance"), IsOwner(send_error_message=True))
|
||||
async def cmd_setbalance(message: Message, command: CommandObject):
|
||||
if not command.args:
|
||||
return await message.answer("Использование: /setbalance <сумма> [@username | реплай]")
|
||||
|
||||
parts = command.args.strip().split(maxsplit=2)
|
||||
try:
|
||||
amount = int(parts[0])
|
||||
except ValueError:
|
||||
return await message.answer("Сумма должна быть числом.")
|
||||
|
||||
user_obj, uid, username = await _get_target(message, parts[1] if len(parts) > 1 else None)
|
||||
target_id = user_obj.id if user_obj else uid
|
||||
|
||||
if not target_id:
|
||||
return await message.answer("Пользователь не найден.")
|
||||
|
||||
await economy.set_balance(target_id, amount, username, user_obj.full_name if user_obj else None)
|
||||
await message.answer(f"Баланс {user_mention(user_obj, username)} → {hbold(fmt(amount))} {CURRENCY_NAME}")
|
||||
return None
|
||||
|
||||
|
||||
@router.message(Command("plusbalance"), IsOwner(send_error_message=True))
|
||||
async def cmd_plusbalance(message: Message, command: CommandObject):
|
||||
if not command.args:
|
||||
return await message.answer("Использование: /plusbalance <сумма> [@username | реплай]")
|
||||
|
||||
parts = command.args.strip().split(maxsplit=2)
|
||||
try:
|
||||
delta = int(parts[0])
|
||||
except ValueError:
|
||||
return await message.answer("Сумма должна быть числом.")
|
||||
|
||||
user_obj, uid, username = await _get_target(message, parts[1] if len(parts) > 1 else None)
|
||||
target_id = user_obj.id if user_obj else uid
|
||||
|
||||
if not target_id:
|
||||
return await message.answer("Пользователь не найден.")
|
||||
|
||||
new_bal = await economy.modify_balance(target_id, delta, username, user_obj.full_name if user_obj else None)
|
||||
await message.answer(f"{user_mention(user_obj, username)} +{fmt(delta)} → {hbold(fmt(new_bal))} {CURRENCY_NAME}")
|
||||
return None
|
||||
|
||||
|
||||
@router.message(Command("minbalance"), IsOwner(send_error_message=True))
|
||||
async def cmd_minbalance(message: Message, command: CommandObject):
|
||||
if not command.args:
|
||||
return await message.answer("Использование: /minbalance <сумма> [@username | реплай]")
|
||||
|
||||
parts = command.args.strip().split(maxsplit=2)
|
||||
try:
|
||||
delta = int(parts[0])
|
||||
except ValueError:
|
||||
return await message.answer("Сумма должна быть числом.")
|
||||
|
||||
user_obj, uid, username = await _get_target(message, parts[1] if len(parts) > 1 else None)
|
||||
target_id = user_obj.id if user_obj else uid
|
||||
|
||||
if not target_id:
|
||||
return await message.answer("Пользователь не найден.")
|
||||
|
||||
new_bal = await economy.modify_balance(target_id, -delta, username, user_obj.full_name if user_obj else None)
|
||||
await message.answer(f"{user_mention(user_obj, username)} -{fmt(delta)} → {hbold(fmt(new_bal))} {CURRENCY_NAME}")
|
||||
return None
|
||||
|
||||
|
||||
@router.message(Command("top"))
|
||||
async def cmd_top(message: Message):
|
||||
top = economy.get_top(20)
|
||||
if not top:
|
||||
return await message.answer("Топ пустой — никто ещё не заработал коины!")
|
||||
|
||||
lines = ["Топ-20 богачей:"]
|
||||
for i, (_, bal, username, full_name) in enumerate(top, 1):
|
||||
medal = ["1st", "2nd", "3rd"][i-1] if i <= 3 else f"{i}."
|
||||
name = f"@{username}" if username else hbold(full_name)
|
||||
lines.append(f"{medal} {name} — {hbold(fmt(bal))} {CURRENCY_NAME}")
|
||||
|
||||
await message.answer("\n".join(lines))
|
||||
return None
|
||||
|
||||
|
||||
@router.message(Command("deletebalance"), IsOwner(send_error_message=True))
|
||||
async def cmd_deletebalance(message: Message):
|
||||
user_obj, uid, username = await _get_target(message)
|
||||
if not (user_obj or uid):
|
||||
return await message.answer("Укажи пользователя реплаем или @username")
|
||||
|
||||
target_id = user_obj.id if user_obj else uid
|
||||
deleted = await economy.delete_user(target_id)
|
||||
name = user_mention(user_obj, username)
|
||||
await message.answer(f"Запись {name} {'удалена' if deleted else 'не существовала'}")
|
||||
return None
|
||||
|
||||
|
||||
# ==================== Запуск ====================
|
||||
async def on_startup(_):
|
||||
await economy.load()
|
||||
|
||||
|
||||
__all__ = ["router", "on_startup", "register_user_on_message"]
|
||||
Reference in New Issue
Block a user