From 60f6c076f5dcf757baacefb8eca4eb554e624415 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 13 Jul 2026 15:02:00 +0000 Subject: [PATCH 1/3] v2.0: FunPay login UI, promo suite, background APK, richer panel Add golden_key login flow, PromoEngine (raise/undercut/SEO/mass ops), stats/broadcast, redesigned web dashboard tabs, Android UI without startup Loading plus foreground/background service, release packaging. Co-authored-by: offjext --- README.md | 8 + android/app/service.py | 56 +++ android/buildozer.spec | 5 +- funpaybot/__init__.py | 2 +- funpaybot/auth.py | 73 ++++ funpaybot/bot.py | 7 +- funpaybot/cli.py | 61 +++ funpaybot/gui.py | 684 ++++++++++++++++++++++++---------- funpaybot/modules/__init__.py | 5 + funpaybot/modules/promo.py | 247 ++++++++++++ funpaybot/modules/stats.py | 84 +++++ pyproject.toml | 2 +- scripts/build_android_apk.sh | 5 +- tests/test_promo.py | 22 ++ 14 files changed, 1048 insertions(+), 213 deletions(-) create mode 100644 android/app/service.py create mode 100644 funpaybot/auth.py create mode 100644 funpaybot/modules/promo.py create mode 100644 funpaybot/modules/stats.py create mode 100644 tests/test_promo.py diff --git a/README.md b/README.md index c624eca..47ef8d4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,14 @@ ## Возможности +- **Вход через FunPay** (`golden_key`) — GUI / APK / `funpaybot login` +- **Вечный онлайн** + авто-промо в фоне (APK foreground service) +- **Продвижение**: поднятие, андеркат, −/+5%, SEO-заголовки, активация лотов, отчёт конкурентов +- **Автоответы / автовыдача / приветствия / отзывы / рассылка** +- **Веб-панель** с вкладками: дашборд, промо, лоты, заказы, чаты, склад + +## Возможности (таблица) + | Модуль | Описание | |--------|----------| | **Автоподнятие лотов** | Циклический `POST /lots/raise` по категориям из конфига | diff --git a/android/app/service.py b/android/app/service.py new file mode 100644 index 0000000..e28e0f0 --- /dev/null +++ b/android/app/service.py @@ -0,0 +1,56 @@ +"""Фоновый Android-сервис FunPayBot (buildozer services).""" + +from __future__ import annotations + +# Этот файл запускается отдельно от UI, когда приложение уходит в фон / сервис стартует. + + +def main() -> None: + import time + from pathlib import Path + + # минимальный keepalive + бот + try: + from funpaybot.bot import FunPayBot + from funpaybot.config import load_config + from funpaybot.modules.promo import PromoEngine + from funpaybot.utils.logging_setup import setup_logging + except Exception: + # если пакет ещё не в path + import sys + + sys.path.insert(0, str(Path(__file__).resolve().parent)) + from funpaybot.bot import FunPayBot + from funpaybot.config import load_config + from funpaybot.modules.promo import PromoEngine + from funpaybot.utils.logging_setup import setup_logging + + cfg = load_config() + if not cfg.golden_key or cfg.golden_key == "your_golden_key_here": + # ждём пока UI сохранит ключ + while True: + cfg = load_config() + if cfg.golden_key and cfg.golden_key != "your_golden_key_here": + break + time.sleep(5) + + cfg.online.enabled = True + setup_logging(cfg.logging) + bot = FunPayBot(cfg) + bot.start() + promo = PromoEngine(bot.client, cfg.raise_lots) + node = None + if cfg.raise_lots.categories and cfg.raise_lots.categories[0].node_id > 0: + node = cfg.raise_lots.categories[0].node_id + promo.start_auto_promo(node_id=node) + + while True: + try: + bot.online.ping_once() + except Exception: + pass + time.sleep(30) + + +if __name__ == "__main__": + main() diff --git a/android/buildozer.spec b/android/buildozer.spec index 4188092..30120db 100644 --- a/android/buildozer.spec +++ b/android/buildozer.spec @@ -4,16 +4,17 @@ package.name = funpaybot package.domain = com.funpaybot source.dir = app source.include_exts = py,png,jpg,kv,atlas,yaml,txt,md -version = 1.0.0 +version = 2.0.0 requirements = python3,kivy,requests,beautifulsoup4,PyYAML,python-dotenv,pydantic,pydantic-settings,certifi,charset-normalizer,idna,urllib3,soupsieve,typing-extensions,annotated-types,pyjnius,android orientation = portrait fullscreen = 0 -android.permissions = INTERNET,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,READ_EXTERNAL_STORAGE +android.permissions = INTERNET,ACCESS_NETWORK_STATE,WRITE_EXTERNAL_STORAGE,READ_EXTERNAL_STORAGE,FOREGROUND_SERVICE,WAKE_LOCK,RECEIVE_BOOT_COMPLETED,POST_NOTIFICATIONS android.api = 33 android.minapi = 24 android.ndk = 25b android.accept_sdk_license = True android.archs = arm64-v8a +services = FunpayService:service.py p4a.branch = master [buildozer] diff --git a/funpaybot/__init__.py b/funpaybot/__init__.py index 60080b3..ce2fd21 100644 --- a/funpaybot/__init__.py +++ b/funpaybot/__init__.py @@ -1,4 +1,4 @@ """FunPayBot — многофункциональная автоматизация для продавцов FunPay.""" -__version__ = "1.0.0" +__version__ = "2.0.0" __all__ = ["__version__"] diff --git a/funpaybot/auth.py b/funpaybot/auth.py new file mode 100644 index 0000000..9acd08a --- /dev/null +++ b/funpaybot/auth.py @@ -0,0 +1,73 @@ +"""Авторизация FunPay по golden_key (+ сохранение сессии).""" + +from __future__ import annotations + +import logging +from pathlib import Path + +from funpaybot.client import AuthError, FunPayClient, FunPayError +from funpaybot.config import AppConfig, load_config +from funpaybot.models import UserInfo + +logger = logging.getLogger(__name__) + + +def save_golden_key(key: str, env_path: str | Path = ".env") -> None: + path = Path(env_path) + key = key.strip() + lines: list[str] = [] + if path.exists(): + lines = path.read_text(encoding="utf-8").splitlines() + found = False + for i, line in enumerate(lines): + if line.startswith("FUNPAY_GOLDEN_KEY="): + lines[i] = f"FUNPAY_GOLDEN_KEY={key}" + found = True + break + if not found: + lines.append(f"FUNPAY_GOLDEN_KEY={key}") + else: + lines = [ + f"FUNPAY_GOLDEN_KEY={key}", + "FUNPAY_USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + ] + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def login_with_golden_key( + golden_key: str, + *, + user_agent: str | None = None, + proxy: dict[str, str] | None = None, + save: bool = True, +) -> tuple[FunPayClient, UserInfo]: + """ + Войти в FunPay по cookie golden_key. + Как получить: браузер → FunPay → F12 → Application → Cookies → golden_key. + """ + key = (golden_key or "").strip() + if not key or key == "your_golden_key_here": + raise AuthError("Вставьте golden_key из cookie FunPay") + + if save: + save_golden_key(key) + + client = FunPayClient(golden_key=key, user_agent=user_agent, proxy=proxy) + try: + user = client.warmup() + except FunPayError: + client.close() + raise + logger.info("Вход выполнен: %s (id=%s)", user.username, user.id) + return client, user + + +def login_from_config(config_path: str | None = None) -> tuple[AppConfig, FunPayClient, UserInfo]: + cfg = load_config(config_path) + client, user = login_with_golden_key( + cfg.golden_key, + user_agent=cfg.user_agent, + proxy=cfg.proxy_dict(), + save=False, + ) + return cfg, client, user diff --git a/funpaybot/bot.py b/funpaybot/bot.py index cd1e2fb..0de9644 100644 --- a/funpaybot/bot.py +++ b/funpaybot/bot.py @@ -17,6 +17,7 @@ Greeter, LotRaiser, PriceManager, + PromoEngine, ReviewReminder, ) from funpaybot.models import ChatMessage, Order @@ -43,6 +44,7 @@ def __init__(self, config: AppConfig) -> None: self.greeter = Greeter(self.client, config.greeting) self.reviews = ReviewReminder(self.client, config.review_reminder) self.pricing = PriceManager(self.client, config.pricing) + self.promo = PromoEngine(self.client, config.raise_lots) self.events = EventListener(self.client, poll_interval=8.0) self._stop = threading.Event() @@ -85,10 +87,12 @@ def start(self) -> None: node_ids = [c.node_id for c in self.config.raise_lots.categories if c.node_id > 0] self.pricing.start(node_ids) + # авто-промо (поднятие + активация + опционально undercut) + self.promo.start_auto_promo(node_id=node_ids[0] if node_ids else None) self.events.start() logger.info( - "Все модули запущены. Вечный онлайн: %s. Ctrl+C для остановки.", + "Все модули запущены. Вечный онлайн: %s. Авто-промо: ON. Ctrl+C для остановки.", self.online.status_text, ) @@ -109,6 +113,7 @@ def stop(self) -> None: self.online.stop() self.reviews.stop() self.pricing.stop() + self.promo.stop_auto_promo() self.client.close() logger.info("Остановлен.") diff --git a/funpaybot/cli.py b/funpaybot/cli.py index 1aed159..b1ddc84 100644 --- a/funpaybot/cli.py +++ b/funpaybot/cli.py @@ -415,6 +415,67 @@ def online_cmd( bot.client.close() +@app.command("login") +def login_cmd( + key: Optional[str] = typer.Option(None, "--key", "-k", help="golden_key (иначе спросит)"), + config: Optional[str] = typer.Option(None, "--config", "-c"), +) -> None: + """Войти в FunPay по golden_key и сохранить сессию.""" + from funpaybot.auth import login_with_golden_key + + setup_logging(load_config(config).logging) + token = key or console.input("[bold]golden_key:[/] ").strip() + try: + client, user = login_with_golden_key(token) + bal = client.get_balance() + console.print(f"[green]Вход OK[/]: {user.username} (id={user.id})") + console.print(bal) + client.close() + except Exception as exc: + console.print(f"[red]{exc}[/]") + raise typer.Exit(1) from exc + + +@app.command("promo") +def promo_cmd( + action: str = typer.Argument("full", help="full|raise|undercut|activate|titles|competitors"), + node_id: Optional[int] = typer.Option(None, "--node-id", "-n"), + game_id: Optional[int] = typer.Option(None, "--game-id", "-g"), + config: Optional[str] = typer.Option(None, "--config", "-c"), +) -> None: + """Продвижение товаров (поднятие, цены, активация, SEO).""" + from funpaybot.modules.promo import PromoEngine + + cfg, client = _make_client(config) + try: + client.warmup() + promo = PromoEngine(client, cfg.raise_lots) + if action == "full": + r = promo.full_promo_cycle(node_id=node_id, game_id=game_id) + console.print(r) + elif action == "raise": + console.print(promo.raise_all_configured()) + elif action == "undercut": + if not node_id: + console.print("[red]нужен --node-id[/]") + raise typer.Exit(2) + console.print(promo.undercut_node(node_id)) + elif action == "activate": + console.print({"activated": promo.activate_all()}) + elif action == "titles": + console.print(promo.boost_titles(dry_run=True)) + elif action == "competitors": + if not node_id: + console.print("[red]нужен --node-id[/]") + raise typer.Exit(2) + console.print(promo.competitor_report(node_id)) + else: + console.print("[red]unknown action[/]") + raise typer.Exit(2) + finally: + client.close() + + @app.command("gui") def gui_cmd( config: Optional[str] = typer.Option(None, "--config", "-c"), diff --git a/funpaybot/gui.py b/funpaybot/gui.py index 244f70c..e8f0771 100644 --- a/funpaybot/gui.py +++ b/funpaybot/gui.py @@ -1,4 +1,4 @@ -"""Локальная веб-панель управления FunPayBot.""" +"""Современная веб-панель FunPayBot: логин + полный функционал.""" from __future__ import annotations @@ -10,168 +10,260 @@ from flask import Flask, jsonify, render_template_string, request from funpaybot import __version__ +from funpaybot.auth import login_with_golden_key, save_golden_key from funpaybot.bot import FunPayBot +from funpaybot.client import AuthError, FunPayError from funpaybot.config import load_config +from funpaybot.modules.promo import PromoEngine +from funpaybot.modules.stats import BroadcastService, StatsService from funpaybot.utils.logging_setup import setup_logging +from funpaybot.utils.stock import ProductStock logger = logging.getLogger(__name__) -PANEL_HTML = """ +PANEL_HTML = r""" - - - FunPayBot - + + +FunPayBot + -
-
-

FunPayBot

- v{{ version }} +
+

FunPayBot

v{{ version }}64-bit
+

Вход через FunPay · вечный онлайн · продвижение · автовыдача · чаты · статистика

+ +
+

Войти через FunPay

+

1) Откройте funpay.com в браузере
2) F12 → Application → Cookies → скопируйте golden_key
3) Вставьте ниже и нажмите «Войти»

+ + +
+ +
-

Панель продавца: вечный онлайн, поднятие лотов, автовыдача и автоответы

+
Ожидание входа…
+
+