Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

## Возможности

- **Вход через FunPay** (`golden_key`) — GUI / APK / `funpaybot login`
- **Вечный онлайн** + авто-промо в фоне (APK foreground service)
- **Продвижение**: поднятие, андеркат, −/+5%, SEO-заголовки, активация лотов, отчёт конкурентов
- **Автоответы / автовыдача / приветствия / отзывы / рассылка**
- **Веб-панель** с вкладками: дашборд, промо, лоты, заказы, чаты, склад

## Возможности (таблица)

| Модуль | Описание |
|--------|----------|
| **Автоподнятие лотов** | Циклический `POST /lots/raise` по категориям из конфига |
Expand Down
570 changes: 503 additions & 67 deletions android/app/main.py

Large diffs are not rendered by default.

8 changes: 0 additions & 8 deletions android/app/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
requests>=2.31.0
beautifulsoup4>=4.12.0
lxml>=5.0.0
PyYAML>=6.0.1
rich>=13.7.0
typer>=0.12.0
python-dotenv>=1.0.0
colorama>=0.4.6
schedule>=1.2.1
pydantic>=2.5.0
pydantic-settings>=2.1.0
flask>=3.0.0
56 changes: 56 additions & 0 deletions android/app/service.py
Original file line number Diff line number Diff line change
@@ -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()
7 changes: 4 additions & 3 deletions android/buildozer.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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
requirements = python3,kivy,requests,beautifulsoup4,PyYAML,python-dotenv,pydantic,pydantic-settings,certifi,charset-normalizer,idna,urllib3,soupsieve,typing-extensions,annotated-types,pyjnius,android
version = 2.1.0
requirements = python3,kivy,requests,beautifulsoup4,PyYAML,python-dotenv,certifi,charset-normalizer,idna,urllib3,soupsieve,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]
Expand Down
2 changes: 1 addition & 1 deletion funpaybot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""FunPayBot — многофункциональная автоматизация для продавцов FunPay."""

__version__ = "1.0.0"
__version__ = "2.1.0"
__all__ = ["__version__"]
75 changes: 75 additions & 0 deletions funpaybot/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Авторизация FunPay по golden_key (+ сохранение сессии)."""

from __future__ import annotations

import logging
from pathlib import Path

from funpaybot.client import AuthError, FunPayClient, FunPayError
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):
from funpaybot.config import load_config

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
7 changes: 6 additions & 1 deletion funpaybot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Greeter,
LotRaiser,
PriceManager,
PromoEngine,
ReviewReminder,
)
from funpaybot.models import ChatMessage, Order
Expand All @@ -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()

Expand Down Expand Up @@ -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,
)

Expand All @@ -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("Остановлен.")

Expand Down
61 changes: 61 additions & 0 deletions funpaybot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Loading
Loading