fix
This commit is contained in:
+43
-6
@@ -23,15 +23,52 @@ class AuthStorage:
|
||||
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def register(self, user_id: int) -> bool:
|
||||
async def register(
|
||||
self, user_id: int, username: str = None, full_name: str = None
|
||||
) -> bool:
|
||||
"""Регистрирует пользователя и освежает его профиль.
|
||||
|
||||
Профиль перезаписывается и для уже известных пользователей — ник в
|
||||
Telegram может смениться, а взять его неоткуда, кроме входящего
|
||||
апдейта.
|
||||
"""
|
||||
async with self._lock:
|
||||
data = await asyncio.to_thread(self._load_sync)
|
||||
if str(user_id) in data:
|
||||
return False
|
||||
data[str(user_id)] = {"registered_at": datetime.now().isoformat()}
|
||||
key = str(user_id)
|
||||
is_new = key not in data
|
||||
record = data.get(key, {})
|
||||
if is_new:
|
||||
record["registered_at"] = datetime.now().isoformat()
|
||||
if username is not None:
|
||||
record["username"] = username
|
||||
if full_name is not None:
|
||||
record["full_name"] = full_name
|
||||
data[key] = record
|
||||
await asyncio.to_thread(self._save_sync, data)
|
||||
logger.info(f"User {user_id} registered")
|
||||
return True
|
||||
if is_new:
|
||||
logger.info(f"User {user_id} registered ({username or full_name or 'no name'})")
|
||||
return is_new
|
||||
|
||||
async def get_profile(self, user_id: int) -> dict:
|
||||
async with self._lock:
|
||||
data = await asyncio.to_thread(self._load_sync)
|
||||
return data.get(str(user_id), {})
|
||||
|
||||
async def get_all_profiles(self) -> dict:
|
||||
"""Возвращает {user_id: профиль} для всех пользователей."""
|
||||
async with self._lock:
|
||||
data = await asyncio.to_thread(self._load_sync)
|
||||
return {int(k): v for k, v in data.items()}
|
||||
|
||||
@staticmethod
|
||||
def display_name(profile: dict) -> str:
|
||||
"""Человекочитаемое имя: @ник, иначе имя, иначе пусто."""
|
||||
if not profile:
|
||||
return ""
|
||||
username = profile.get("username")
|
||||
if username:
|
||||
return f"@{username}"
|
||||
return profile.get("full_name") or ""
|
||||
|
||||
async def delete_user(self, user_id: int) -> bool:
|
||||
async with self._lock:
|
||||
|
||||
+171
-98
@@ -97,13 +97,15 @@ class BotInterface:
|
||||
latest_msg_id=message.message_id
|
||||
)
|
||||
|
||||
# Пароля для входа нет — личность подтверждает сам Telegram.
|
||||
# Первый /start заодно регистрирует пользователя; для остальных
|
||||
# обновляем ник, он мог смениться.
|
||||
await interface._remember_profile(message.from_user)
|
||||
|
||||
if interface.auth_manager.is_authenticated(user_id) and interface.auth_manager.is_admin(user_id):
|
||||
await interface._show_main_menu(message)
|
||||
return
|
||||
|
||||
# Пароля для входа нет — личность подтверждает сам Telegram.
|
||||
# Первый /start заодно регистрирует пользователя.
|
||||
await interface.auth_storage.register(user_id)
|
||||
if not interface.auth_manager.is_authenticated(user_id):
|
||||
interface.auth_manager.login(user_id, is_admin=False)
|
||||
await interface._show_user_menu(message)
|
||||
@@ -1140,6 +1142,37 @@ class BotInterface:
|
||||
if not await require_admin(callback):
|
||||
return
|
||||
await interface._show_users_list(callback.message, edit=True)
|
||||
|
||||
# === Листание админских списков ===
|
||||
def _page_arg(data: str, prefix: str) -> int:
|
||||
try:
|
||||
return int(data.replace(prefix, "", 1))
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
@dp.callback_query(F.data.startswith("users_page_"))
|
||||
async def cb_users_page(callback: CallbackQuery):
|
||||
if not await require_admin(callback):
|
||||
return
|
||||
page = _page_arg(callback.data, "users_page_")
|
||||
await interface._show_users_list(callback.message, edit=True, page=page)
|
||||
await callback.answer()
|
||||
|
||||
@dp.callback_query(F.data.startswith("streamers_page_"))
|
||||
async def cb_streamers_page(callback: CallbackQuery):
|
||||
if not await require_admin(callback):
|
||||
return
|
||||
page = _page_arg(callback.data, "streamers_page_")
|
||||
await interface._show_streamers_list(callback.message, edit=True, page=page)
|
||||
await callback.answer()
|
||||
|
||||
@dp.callback_query(F.data.startswith("tasks_page_"))
|
||||
async def cb_tasks_page(callback: CallbackQuery):
|
||||
if not await require_admin(callback):
|
||||
return
|
||||
page = _page_arg(callback.data, "tasks_page_")
|
||||
await interface._show_tasks_list(callback.message, edit=True, page=page)
|
||||
await callback.answer()
|
||||
await callback.answer()
|
||||
|
||||
@dp.callback_query(F.data.startswith("udetail_"))
|
||||
@@ -1562,6 +1595,45 @@ class BotInterface:
|
||||
await self._safe_delete(message.bot, message.chat.id, self._menu_msg.pop(user_id))
|
||||
await self._safe_delete(message.bot, message.chat.id, message.message_id)
|
||||
|
||||
# Записей на страницу. Telegram режет сообщение на 4096 символах и
|
||||
# 100 кнопках — поэтому списки листаем, а не показываем целиком.
|
||||
_PAGE_SIZE = 8
|
||||
|
||||
@staticmethod
|
||||
def _paginate(items: list, page: int, page_size: int = None) -> tuple:
|
||||
"""Возвращает (срез страницы, нормализованный номер, всего страниц)."""
|
||||
page_size = page_size or BotInterface._PAGE_SIZE
|
||||
total_pages = max(1, (len(items) + page_size - 1) // page_size)
|
||||
page = max(0, min(page, total_pages - 1))
|
||||
start = page * page_size
|
||||
return items[start:start + page_size], page, total_pages
|
||||
|
||||
@staticmethod
|
||||
def _add_nav(builder, prefix: str, page: int, total_pages: int) -> None:
|
||||
"""Добавляет строку навигации «◀ 2/5 ▶», если страниц больше одной."""
|
||||
if total_pages <= 1:
|
||||
return
|
||||
row = []
|
||||
if page > 0:
|
||||
row.append(InlineKeyboardButton(text="◀️", callback_data=f"{prefix}{page - 1}"))
|
||||
row.append(InlineKeyboardButton(text=f"{page + 1}/{total_pages}", callback_data="noop"))
|
||||
if page < total_pages - 1:
|
||||
row.append(InlineKeyboardButton(text="▶️", callback_data=f"{prefix}{page + 1}"))
|
||||
builder.row(*row)
|
||||
|
||||
async def _remember_profile(self, from_user) -> None:
|
||||
"""Сохраняет/освежает ник пользователя из входящего апдейта."""
|
||||
if not from_user:
|
||||
return
|
||||
try:
|
||||
await self.auth_storage.register(
|
||||
from_user.id,
|
||||
username=from_user.username or "",
|
||||
full_name=(from_user.full_name or "").strip(),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to remember profile for {from_user.id}: {e}")
|
||||
|
||||
async def _edit_or_send(self, message: Message, text: str, markup, edit: bool, user_id: int = None) -> None:
|
||||
if edit:
|
||||
try:
|
||||
@@ -1611,116 +1683,100 @@ class BotInterface:
|
||||
await self._track_msg(uid, sent1.message_id)
|
||||
await self._track_msg(uid, sent2.message_id)
|
||||
|
||||
async def _show_streamers_list(self, message: Message, edit: bool = False):
|
||||
async def _show_streamers_list(self, message: Message, edit: bool = False, page: int = 0):
|
||||
active = await self.task_manager.get_active_tasks()
|
||||
completed = await self.task_manager.get_completed_tasks()
|
||||
|
||||
twitch_active = {tid: p for tid, p in active.items() if p.task_type == "twitch_irc"}
|
||||
twitch_done = {tid: p for tid, p in completed.items() if p.task_type == "twitch_irc"}
|
||||
# Плоский список секция→задача: листаем сквозь обе секции,
|
||||
# заголовок печатаем при смене секции внутри страницы
|
||||
entries = [("🔄 Активные", tid, p) for tid, p in active.items() if p.task_type == "twitch_irc"]
|
||||
entries += [("📁 Завершённые", tid, p) for tid, p in completed.items() if p.task_type == "twitch_irc"]
|
||||
|
||||
if not twitch_active and not twitch_done:
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
if not entries:
|
||||
text = "📺 Стримеры\n\nЗадач нет. Отправьте название канала для добавления."
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||||
else:
|
||||
text = "📺 Стримеры\n\n"
|
||||
builder = InlineKeyboardBuilder()
|
||||
page_items, page, total_pages = self._paginate(entries, page)
|
||||
text = f"📺 Стримеры ({len(entries)})"
|
||||
if total_pages > 1:
|
||||
text += f" — страница {page + 1}/{total_pages}"
|
||||
text += "\n\n"
|
||||
|
||||
if twitch_active:
|
||||
text += "🔄 Активные:\n"
|
||||
for tid, p in list(twitch_active.items())[:10]:
|
||||
emoji = p.get_status_emoji()
|
||||
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
|
||||
current_section = None
|
||||
for section, tid, p in page_items:
|
||||
if section != current_section:
|
||||
text += f"{section}:\n"
|
||||
current_section = section
|
||||
emoji = p.get_status_emoji()
|
||||
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
|
||||
text += (
|
||||
f"{emoji} {p.channel}\n"
|
||||
f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n"
|
||||
)
|
||||
suffix = f"🔗{p.links_found}" if section.startswith("🔄") else "завершена"
|
||||
builder.row(InlineKeyboardButton(
|
||||
text=f"{emoji} {p.channel} ({suffix})",
|
||||
callback_data=f"tdetail_{tid}"
|
||||
))
|
||||
|
||||
text += (
|
||||
f"{emoji} {p.channel}\n"
|
||||
f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n"
|
||||
)
|
||||
builder.row(InlineKeyboardButton(
|
||||
text=f"{emoji} {p.channel} (🔗{p.links_found})",
|
||||
callback_data=f"tdetail_{tid}"
|
||||
))
|
||||
|
||||
if twitch_done:
|
||||
text += "\n📁 Завершенные:\n"
|
||||
for tid, p in list(twitch_done.items())[:5]:
|
||||
emoji = p.get_status_emoji()
|
||||
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
|
||||
|
||||
text += (
|
||||
f"{emoji} {p.channel}\n"
|
||||
f" 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n"
|
||||
)
|
||||
builder.row(InlineKeyboardButton(
|
||||
text=f"{emoji} {p.channel} (завершена)",
|
||||
callback_data=f"tdetail_{tid}"
|
||||
))
|
||||
|
||||
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||||
self._add_nav(builder, "streamers_page_", page, total_pages)
|
||||
|
||||
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||||
await self._edit_or_send(message, text, builder.as_markup(), edit)
|
||||
|
||||
async def _show_tasks_list(self, message: Message, edit: bool = False):
|
||||
async def _show_tasks_list(self, message: Message, edit: bool = False, page: int = 0):
|
||||
"""Все задачи — посещения и мониторинг."""
|
||||
active = await self.task_manager.get_active_tasks()
|
||||
completed = await self.task_manager.get_completed_tasks()
|
||||
|
||||
visit_active = {tid: p for tid, p in active.items() if p.task_type in ("visit", "user_visit")}
|
||||
visit_done = {tid: p for tid, p in completed.items() if p.task_type in ("visit", "user_visit")}
|
||||
twitch_active = {tid: p for tid, p in active.items() if p.task_type == "twitch_irc"}
|
||||
twitch_done = {tid: p for tid, p in completed.items() if p.task_type == "twitch_irc"}
|
||||
def _pick(src, types):
|
||||
return [(tid, p) for tid, p in src.items() if p.task_type in types]
|
||||
|
||||
# Плоский список во всех четырёх секциях — листаем сквозной пагинацией
|
||||
entries = [("📺 Twitch — активные", t, p) for t, p in _pick(active, ("twitch_irc",))]
|
||||
entries += [("📺 Twitch — завершённые", t, p) for t, p in _pick(completed, ("twitch_irc",))]
|
||||
entries += [("🔗 Посещения — активные", t, p) for t, p in _pick(active, ("visit", "user_visit"))]
|
||||
entries += [("🔗 Посещения — завершённые", t, p) for t, p in _pick(completed, ("visit", "user_visit"))]
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
if not any([visit_active, visit_done, twitch_active, twitch_done]):
|
||||
if not entries:
|
||||
text = "📊 Задачи\n\nЗадач нет."
|
||||
else:
|
||||
text = "📊 Задачи\n\n"
|
||||
page_items, page, total_pages = self._paginate(entries, page)
|
||||
text = f"📊 Задачи ({len(entries)})"
|
||||
if total_pages > 1:
|
||||
text += f" — страница {page + 1}/{total_pages}"
|
||||
text += "\n\n"
|
||||
|
||||
if twitch_active:
|
||||
text += "📺 Twitch — активные:\n"
|
||||
for tid, p in list(twitch_active.items())[:10]:
|
||||
em = p.get_status_emoji()
|
||||
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
|
||||
text += f"{em} {p.channel}\n 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n"
|
||||
builder.row(InlineKeyboardButton(
|
||||
text=f"{em} 📺 {p.channel} (🔗{p.links_found})",
|
||||
callback_data=f"tdetail_{tid}"
|
||||
))
|
||||
|
||||
if twitch_done:
|
||||
text += "\n📺 Twitch — завершённые:\n"
|
||||
for tid, p in list(twitch_done.items())[:5]:
|
||||
em = p.get_status_emoji()
|
||||
text += f"{em} {p.channel}\n"
|
||||
builder.row(InlineKeyboardButton(
|
||||
text=f"{em} 📺 {p.channel} (завершена)",
|
||||
callback_data=f"tdetail_{tid}"
|
||||
))
|
||||
|
||||
if visit_active:
|
||||
text += "\n🔗 Посещения — активные:\n"
|
||||
for tid, p in list(visit_active.items())[:10]:
|
||||
em = p.get_status_emoji()
|
||||
current_section = None
|
||||
for section, tid, p in page_items:
|
||||
if section != current_section:
|
||||
text += f"\n{section}:\n" if current_section else f"{section}:\n"
|
||||
current_section = section
|
||||
em = p.get_status_emoji()
|
||||
is_active = "активные" in section
|
||||
if section.startswith("📺"):
|
||||
if is_active:
|
||||
domains = ", ".join(p.allowed_domains) if p.allowed_domains else "все"
|
||||
text += f"{em} {p.channel}\n 🔗{p.links_found} | 📊{p.total_visits} | 🌐{domains}\n"
|
||||
label = f"{em} 📺 {p.channel} (🔗{p.links_found})"
|
||||
else:
|
||||
text += f"{em} {p.channel}\n"
|
||||
label = f"{em} 📺 {p.channel} (завершена)"
|
||||
else:
|
||||
url_s = (p.url or "")[:35]
|
||||
uid_tag = f" | uid:{p.user_id}" if p.user_id else ""
|
||||
text += f"{em} {url_s}{uid_tag}\n ✅{p.successful_visits}/{p.max_visits} | 📊{p.total_visits}\n"
|
||||
builder.row(InlineKeyboardButton(
|
||||
text=f"{em} 🔗 {url_s[:28]} ({p.successful_visits}/{p.max_visits})",
|
||||
callback_data=f"tdetail_{tid}"
|
||||
))
|
||||
if is_active:
|
||||
text += f"{em} {url_s}{uid_tag}\n ✅{p.successful_visits}/{p.max_visits} | 📊{p.total_visits}\n"
|
||||
label = f"{em} 🔗 {url_s[:28]} ({p.successful_visits}/{p.max_visits})"
|
||||
else:
|
||||
text += f"{em} {url_s}{uid_tag}\n"
|
||||
label = f"{em} 🔗 {url_s[:28]} (завершена)"
|
||||
builder.row(InlineKeyboardButton(text=label, callback_data=f"tdetail_{tid}"))
|
||||
|
||||
if visit_done:
|
||||
text += "\n🔗 Посещения — завершённые:\n"
|
||||
for tid, p in list(visit_done.items())[:5]:
|
||||
em = p.get_status_emoji()
|
||||
url_s = (p.url or "")[:35]
|
||||
uid_tag = f" | uid:{p.user_id}" if p.user_id else ""
|
||||
text += f"{em} {url_s}{uid_tag}\n"
|
||||
builder.row(InlineKeyboardButton(
|
||||
text=f"{em} 🔗 {url_s[:28]} (завершена)",
|
||||
callback_data=f"tdetail_{tid}"
|
||||
))
|
||||
self._add_nav(builder, "tasks_page_", page, total_pages)
|
||||
|
||||
builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="menu_tasks"))
|
||||
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||||
@@ -3070,21 +3126,27 @@ class BotInterface:
|
||||
else:
|
||||
await self._send_temp(message, "Ошибка запуска задачи")
|
||||
|
||||
async def _show_users_list(self, message: Message, edit: bool = False):
|
||||
all_user_ids, all_tasks, all_clicks, all_rub = await asyncio.gather(
|
||||
self.auth_storage.get_all_user_ids(),
|
||||
async def _show_users_list(self, message: Message, edit: bool = False, page: int = 0):
|
||||
profiles, all_tasks, all_clicks, all_rub = await asyncio.gather(
|
||||
self.auth_storage.get_all_profiles(),
|
||||
self.task_manager.get_all_tasks(),
|
||||
self.balance_storage.get_all(),
|
||||
self.rub_storage.get_all(),
|
||||
)
|
||||
all_user_ids = list(profiles.keys())
|
||||
|
||||
text = f"👥 Пользователи ({len(all_user_ids)})\n\n"
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
if not all_user_ids:
|
||||
text += "Нет зарегистрированных пользователей."
|
||||
text = "👥 Пользователи (0)\n\nНет зарегистрированных пользователей."
|
||||
else:
|
||||
for uid in all_user_ids[:20]:
|
||||
page_ids, page, total_pages = self._paginate(all_user_ids, page)
|
||||
text = f"👥 Пользователи ({len(all_user_ids)})"
|
||||
if total_pages > 1:
|
||||
text += f" — страница {page + 1}/{total_pages}"
|
||||
text += "\n\n"
|
||||
|
||||
for uid in page_ids:
|
||||
clicks = all_clicks.get(str(uid), 0)
|
||||
rub = all_rub.get(str(uid), 0)
|
||||
active = sum(
|
||||
@@ -3096,24 +3158,35 @@ class BotInterface:
|
||||
bal_str += f" 🪙{rub}₽"
|
||||
if active:
|
||||
bal_str += f" 📋{active}"
|
||||
text += f"👤 `{uid}` — {bal_str}\n"
|
||||
# Ник известен только после того, как пользователь напишет боту
|
||||
name = AuthStorage.display_name(profiles.get(uid))
|
||||
text += f"👤 {name + ' ' if name else ''}`{uid}` — {bal_str}\n"
|
||||
builder.row(InlineKeyboardButton(
|
||||
text=f"👤 {uid} {bal_str}",
|
||||
text=f"👤 {name or uid} {bal_str}",
|
||||
callback_data=f"udetail_{uid}",
|
||||
))
|
||||
|
||||
self._add_nav(builder, "users_page_", page, total_pages)
|
||||
|
||||
builder.row(InlineKeyboardButton(text="🔙 В меню", callback_data="menu_main"))
|
||||
await self._edit_or_send(message, text, builder.as_markup(), edit)
|
||||
|
||||
async def _show_user_detail(self, callback: CallbackQuery, target_user_id: int):
|
||||
clicks = await self.balance_storage.get_balance(target_user_id)
|
||||
rub = await self.rub_storage.get_balance(target_user_id)
|
||||
profile = await self.auth_storage.get_profile(target_user_id)
|
||||
all_tasks = await self.task_manager.get_all_tasks()
|
||||
user_tasks = {tid: p for tid, p in all_tasks.items() if p.user_id == target_user_id}
|
||||
active = sum(1 for p in user_tasks.values() if not p.completed and not p.stopped)
|
||||
|
||||
name = AuthStorage.display_name(profile)
|
||||
header = f"👤 {name} ({target_user_id})" if name else f"👤 Пользователь {target_user_id}"
|
||||
full_name = profile.get("full_name")
|
||||
if full_name and name != full_name:
|
||||
header += f"\n📝 {full_name}"
|
||||
|
||||
text = (
|
||||
f"👤 Пользователь {target_user_id}\n\n"
|
||||
f"{header}\n\n"
|
||||
f"🖱 Переходов: {clicks}\n"
|
||||
f"🪙 Рублей: {rub} ₽\n"
|
||||
f"📋 Задач: {len(user_tasks)} (активных: {active})\n"
|
||||
|
||||
Reference in New Issue
Block a user