fpx-engine 0.7.4__tar.gz → 0.7.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/PKG-INFO +9 -5
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/README.md +8 -4
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/__init__.py +2 -2
- fpx_engine-0.7.6/fpx/_api/_client.py +173 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/_parsers/__init__.py +3 -2
- fpx_engine-0.7.6/fpx/_parsers/_base.py +16 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/_parsers/_category.py +11 -12
- fpx_engine-0.7.6/fpx/_parsers/_chats.py +116 -0
- fpx_engine-0.7.6/fpx/_parsers/_lots.py +191 -0
- fpx_engine-0.7.6/fpx/_parsers/_orders.py +149 -0
- fpx_engine-0.7.6/fpx/_parsers/_profile.py +220 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/account.py +10 -8
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/addons.py +8 -16
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/category.py +17 -19
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/chat.py +35 -35
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/editor.py +46 -69
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/lot.py +36 -39
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/order.py +25 -25
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/profile.py +63 -49
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/account/subclasses/review.py +9 -9
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/runner.py +35 -34
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/subclasses/_category.py +12 -18
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/subclasses/_chat.py +69 -55
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/subclasses/_order.py +30 -29
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/subclasses/_review.py +13 -12
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/classes/runner/subclasses/router.py +98 -91
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/main.py +22 -12
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/middlewares/_request_engine.py +18 -17
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/models/__init__.py +1 -1
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/models/account.py +26 -20
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/models/chat.py +8 -9
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/models/lots.py +58 -49
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/services/__init__.py +8 -8
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/errors.py +104 -41
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/storage/base.py +5 -5
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/storage/file.py +6 -6
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/storage/memory.py +5 -5
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/storage/redis.py +7 -8
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx_engine.egg-info/PKG-INFO +9 -5
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx_engine.egg-info/SOURCES.txt +1 -2
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/pyproject.toml +1 -1
- fpx_engine-0.7.4/fpx/_api/_client.py +0 -211
- fpx_engine-0.7.4/fpx/_parsers/_base.py +0 -16
- fpx_engine-0.7.4/fpx/_parsers/_chats.py +0 -113
- fpx_engine-0.7.4/fpx/_parsers/_lots.py +0 -194
- fpx_engine-0.7.4/fpx/_parsers/_orders.py +0 -152
- fpx_engine-0.7.4/fpx/_parsers/_profile.py +0 -214
- fpx_engine-0.7.4/tests/test_happy_path.py +0 -43
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/LICENSE +0 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/fsm/__init__.py +0 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/fsm/fsm.py +0 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/py.typed +0 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/dependencies.py +0 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx/utils/storage/__init__.py +0 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx_engine.egg-info/dependency_links.txt +0 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx_engine.egg-info/requires.txt +0 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/fpx_engine.egg-info/top_level.txt +0 -0
- {fpx_engine-0.7.4 → fpx_engine-0.7.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fpx-engine
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.6
|
|
4
4
|
Summary: Автоматизация работы с FunPay
|
|
5
5
|
Author-email: Be My Code <gettofarmila@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/bymyforge/fpx
|
|
@@ -76,19 +76,23 @@ pip install -U fpx-engine
|
|
|
76
76
|
import asyncio
|
|
77
77
|
from fpx import FunPayTools, types
|
|
78
78
|
|
|
79
|
+
|
|
79
80
|
async def main():
|
|
80
81
|
# инициализируем аккаунт (golden_key и golden_seal - куки твоего аккаунта на funpay.com)
|
|
81
|
-
fp = FunPayTools(
|
|
82
|
+
fp = FunPayTools("golden_key", "golden_seal")
|
|
83
|
+
|
|
82
84
|
# ловим сообщение
|
|
83
85
|
@fp.router.on_message()
|
|
84
86
|
async def answer_message(message: types.Message):
|
|
85
87
|
# отвечаем на сообщение
|
|
86
|
-
await message.answer(
|
|
87
|
-
|
|
88
|
+
await message.answer("Привет")
|
|
89
|
+
|
|
90
|
+
# запускаем приём событий
|
|
88
91
|
await fp.runner.start_polling(3, is_background=True)
|
|
89
92
|
await fp.runner.idle()
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
92
96
|
asyncio.run(main())
|
|
93
97
|
```
|
|
94
98
|
|
|
@@ -51,19 +51,23 @@ pip install -U fpx-engine
|
|
|
51
51
|
import asyncio
|
|
52
52
|
from fpx import FunPayTools, types
|
|
53
53
|
|
|
54
|
+
|
|
54
55
|
async def main():
|
|
55
56
|
# инициализируем аккаунт (golden_key и golden_seal - куки твоего аккаунта на funpay.com)
|
|
56
|
-
fp = FunPayTools(
|
|
57
|
+
fp = FunPayTools("golden_key", "golden_seal")
|
|
58
|
+
|
|
57
59
|
# ловим сообщение
|
|
58
60
|
@fp.router.on_message()
|
|
59
61
|
async def answer_message(message: types.Message):
|
|
60
62
|
# отвечаем на сообщение
|
|
61
|
-
await message.answer(
|
|
62
|
-
|
|
63
|
+
await message.answer("Привет")
|
|
64
|
+
|
|
65
|
+
# запускаем приём событий
|
|
63
66
|
await fp.runner.start_polling(3, is_background=True)
|
|
64
67
|
await fp.runner.idle()
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
67
71
|
asyncio.run(main())
|
|
68
72
|
```
|
|
69
73
|
|
|
@@ -7,7 +7,7 @@ from .main import FunPayTools as FunPayTools
|
|
|
7
7
|
from .utils import errors as errors
|
|
8
8
|
from .utils.dependencies import Dependency as Dependency
|
|
9
9
|
|
|
10
|
-
logger = logging.getLogger(
|
|
10
|
+
logger = logging.getLogger("fpx")
|
|
11
11
|
logger.addHandler(logging.NullHandler())
|
|
12
12
|
|
|
13
|
-
__all__ = [
|
|
13
|
+
__all__ = ["Runner", "Router", "FunPayTools", "types", "errors", "Dependency"]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FunPayClient:
|
|
5
|
+
def __init__(self, account, http_client):
|
|
6
|
+
self._account = account
|
|
7
|
+
self.client = http_client
|
|
8
|
+
|
|
9
|
+
async def get_chats_page(self) -> str:
|
|
10
|
+
r = await self._account._request_engine.execute("GET", "/chat/")
|
|
11
|
+
return r.text
|
|
12
|
+
|
|
13
|
+
async def get_finance_page(self) -> str:
|
|
14
|
+
r = await self._account._request_engine.execute("GET", "/account/balance")
|
|
15
|
+
return r.text
|
|
16
|
+
|
|
17
|
+
async def send_message_request(self, node_name, last_msg, text):
|
|
18
|
+
request_data = {
|
|
19
|
+
"action": "chat_message",
|
|
20
|
+
"data": {"node": node_name, "last_message": last_msg, "content": text},
|
|
21
|
+
}
|
|
22
|
+
payload = {"request": json.dumps(request_data)}
|
|
23
|
+
headers = {
|
|
24
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
25
|
+
"Referer": f"https://funpay.com/chat/?node={node_name.split('-')[-1]}",
|
|
26
|
+
}
|
|
27
|
+
r = await self._account._request_engine.execute("POST", "/runner/", data=payload, headers=headers)
|
|
28
|
+
return r.json()
|
|
29
|
+
|
|
30
|
+
async def send_image_request(self, node_name, last_msg, image_id):
|
|
31
|
+
request_data = {
|
|
32
|
+
"action": "chat_message",
|
|
33
|
+
"data": {"node": node_name, "last_message": last_msg, "content": "", "image_id": image_id},
|
|
34
|
+
}
|
|
35
|
+
payload = {"request": json.dumps(request_data)}
|
|
36
|
+
headers = {
|
|
37
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
38
|
+
"Referer": f"https://funpay.com/chat/?node={node_name.split('-')[-1]}",
|
|
39
|
+
}
|
|
40
|
+
r = await self._account._request_engine.execute("POST", "/runner/", data=payload, headers=headers)
|
|
41
|
+
return r.json()
|
|
42
|
+
|
|
43
|
+
async def get_current_chat(self, chat_id):
|
|
44
|
+
r = await self._account._request_engine.execute("GET", f"/chat/?node={chat_id}")
|
|
45
|
+
return r.text
|
|
46
|
+
|
|
47
|
+
async def get_user_profile(self, user_id):
|
|
48
|
+
r = await self._account._request_engine.execute("GET", f"/users/{user_id}/")
|
|
49
|
+
return r.text
|
|
50
|
+
|
|
51
|
+
async def lot_menu_by_category(self, category_id):
|
|
52
|
+
r = await self._account._request_engine.execute("GET", f"/lots/{category_id}/trade")
|
|
53
|
+
return r.text
|
|
54
|
+
|
|
55
|
+
async def get_main_menu(self):
|
|
56
|
+
r = await self._account._request_engine.execute("GET", "/")
|
|
57
|
+
return r.text
|
|
58
|
+
|
|
59
|
+
async def raise_lot(self, node_id, game_id):
|
|
60
|
+
payload = {"game_id": game_id, "node_id": node_id}
|
|
61
|
+
headers = {"X-Requested-With": "XMLHttpRequest"}
|
|
62
|
+
r = await self._account._request_engine.execute("POST", "/lots/raise", data=payload, headers=headers)
|
|
63
|
+
if "application/json" in r.headers.get("Content-Type", ""):
|
|
64
|
+
response = r.json()
|
|
65
|
+
return response.get("msg")
|
|
66
|
+
else:
|
|
67
|
+
return {"error": "not_json", "status": r.status_code}
|
|
68
|
+
|
|
69
|
+
async def get_lot_info(self, lot_id):
|
|
70
|
+
r = await self._account._request_engine.execute("GET", f"/lots/offer?id={lot_id}")
|
|
71
|
+
return r.text
|
|
72
|
+
|
|
73
|
+
async def get_my_sells(self):
|
|
74
|
+
r = await self._account._request_engine.execute("GET", "/orders/trade")
|
|
75
|
+
return r.text
|
|
76
|
+
|
|
77
|
+
async def refund_order(self, order_id):
|
|
78
|
+
url = "/orders/refund"
|
|
79
|
+
payload = {"id": order_id}
|
|
80
|
+
r = await self._account._request_engine.execute("POST", url, data=payload)
|
|
81
|
+
return r
|
|
82
|
+
|
|
83
|
+
async def get_order_info(self, order_id):
|
|
84
|
+
r = await self._account._request_engine.execute("GET", f"/orders/{order_id}/")
|
|
85
|
+
return r.text
|
|
86
|
+
|
|
87
|
+
async def get_lot_editor_data(self, lot_id):
|
|
88
|
+
r = await self._account._request_engine.execute("GET", f"/lots/offerEdit?offer={lot_id}")
|
|
89
|
+
return r.text
|
|
90
|
+
|
|
91
|
+
async def get_node_editor_data(self, node_id):
|
|
92
|
+
r = await self._account._request_engine.execute("GET", f"/lots/offerEdit?node={node_id}")
|
|
93
|
+
return r.text
|
|
94
|
+
|
|
95
|
+
async def edit_lot(self, lot, active=None):
|
|
96
|
+
payload = {
|
|
97
|
+
"form_created_at": lot.form_created_at,
|
|
98
|
+
"offer_id": lot.offer_id,
|
|
99
|
+
"node_id": lot.node_id,
|
|
100
|
+
"location": lot.location if lot.location else "offer",
|
|
101
|
+
"deleted": lot.deleted,
|
|
102
|
+
}
|
|
103
|
+
payload.update(lot.fields)
|
|
104
|
+
payload.pop("query", None)
|
|
105
|
+
if active is not None:
|
|
106
|
+
if active:
|
|
107
|
+
payload["active"] = "on"
|
|
108
|
+
else:
|
|
109
|
+
payload.pop("active", None)
|
|
110
|
+
headers = {
|
|
111
|
+
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
112
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
113
|
+
"Referer": f"https://funpay.com/lots/offerEdit?node={lot.node_id}&offer={lot.offer_id}&location=offer",
|
|
114
|
+
}
|
|
115
|
+
r = await self._account._request_engine.execute("POST", "/lots/offerSave", data=payload, headers=headers)
|
|
116
|
+
return r
|
|
117
|
+
|
|
118
|
+
async def create_lot(self, lot):
|
|
119
|
+
payload = {
|
|
120
|
+
"form_created_at": lot._form_created_at,
|
|
121
|
+
"offer_id": lot._offer_id,
|
|
122
|
+
"node_id": lot._node_id,
|
|
123
|
+
"location": lot._location,
|
|
124
|
+
"deleted": lot._deleted,
|
|
125
|
+
"active": "on",
|
|
126
|
+
}
|
|
127
|
+
fields = {f.key: f.value for f in lot.fields}
|
|
128
|
+
payload.update(fields)
|
|
129
|
+
payload.pop("query", None)
|
|
130
|
+
headers = {
|
|
131
|
+
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
132
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
133
|
+
"Referer": f"https://funpay.com/lots/offerEdit?node={lot._node_id}",
|
|
134
|
+
"Origin": "https://funpay.com",
|
|
135
|
+
}
|
|
136
|
+
r = await self._account._request_engine.execute("POST", "/lots/offerSave", data=payload, headers=headers)
|
|
137
|
+
return r
|
|
138
|
+
|
|
139
|
+
async def answer_review(self, authorid: str, text: str, orderid: str):
|
|
140
|
+
payload = {"authorId": authorid, "text": text, "rating": "", "orderId": orderid}
|
|
141
|
+
headers = {"X-Requested-With": "XMLHttpRequest"}
|
|
142
|
+
response = await self._account._request_engine.execute("POST", "/orders/review", data=payload, headers=headers)
|
|
143
|
+
return response
|
|
144
|
+
|
|
145
|
+
async def get_chip_category(self, chip_category_id):
|
|
146
|
+
r = await self._account._request_engine.execute("GET", f"/chips/{chip_category_id}/")
|
|
147
|
+
return r.text
|
|
148
|
+
|
|
149
|
+
async def get_lot_category(self, lot_category_id):
|
|
150
|
+
r = await self._account._request_engine.execute("GET", f"/lots/{lot_category_id}/")
|
|
151
|
+
return r.text
|
|
152
|
+
|
|
153
|
+
async def upload_image(self, file_bytes):
|
|
154
|
+
headers = {"X-Requested-With": "XMLHttpRequest"}
|
|
155
|
+
r = await self.client.request(
|
|
156
|
+
"POST", "/file/addChatImage", files={"file": ("image.png", file_bytes, "image/png")}, headers=headers
|
|
157
|
+
)
|
|
158
|
+
return r.json()
|
|
159
|
+
|
|
160
|
+
async def find_category(self, target):
|
|
161
|
+
headers = {"X-Requested-With": "XMLHttpRequest"}
|
|
162
|
+
r = await self.client.request("POST", "/games/promoFilter", data={"query": target}, headers=headers)
|
|
163
|
+
return r.json()
|
|
164
|
+
|
|
165
|
+
async def calc_category_price(self, price, node_id):
|
|
166
|
+
headers = {"X-Requested-With": "XMLHttpRequest"}
|
|
167
|
+
r = await self.client.request("POST", "/lots/calc", data={"nodeId": node_id, "price": price}, headers=headers)
|
|
168
|
+
return r.json()
|
|
169
|
+
|
|
170
|
+
async def get_next_sells(self, next_page_id):
|
|
171
|
+
headers = {"X-Requested-With": "XMLHttpRequest"}
|
|
172
|
+
r = await self.client.request("POST", "/orders/trade", data={"continue": next_page_id}, headers=headers)
|
|
173
|
+
return r.text
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseParser:
|
|
7
|
+
@staticmethod
|
|
8
|
+
def _safe_parse_links(html_content: str, pattern: str):
|
|
9
|
+
"""Fallback метод"""
|
|
10
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
11
|
+
return soup.find_all("a", href=re.compile(pattern))
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def clean_text(element) -> str:
|
|
15
|
+
"""Безопасный сбор текста, если элемент не найден, вернет пустую строку"""
|
|
16
|
+
return element.text.strip() if element else ""
|
|
@@ -10,24 +10,23 @@ logger = logging.getLogger("fpx.category_parser")
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class CategoryParser(BaseParser):
|
|
13
|
-
|
|
14
13
|
@classmethod
|
|
15
14
|
def parse_all_categories(cls, html_content):
|
|
16
|
-
|
|
17
|
-
soup = BeautifulSoup(html_content,
|
|
18
|
-
items = soup.find_all(
|
|
15
|
+
"""Парсит главную страницу фп на лоты"""
|
|
16
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
17
|
+
items = soup.find_all("div", class_="promo-game-item")
|
|
19
18
|
result = []
|
|
20
19
|
for item in items:
|
|
21
|
-
game_title = item.find(
|
|
20
|
+
game_title = item.find("div", class_="game-title")
|
|
22
21
|
game_name = game_title.get_text(strip=True)
|
|
23
|
-
data_id = game_title.get(
|
|
24
|
-
subcategory_str = item.find(
|
|
25
|
-
subcategory_list = subcategory_str.find_all(
|
|
22
|
+
data_id = game_title.get("data-id")
|
|
23
|
+
subcategory_str = item.find("ul", class_="list-inline")
|
|
24
|
+
subcategory_list = subcategory_str.find_all("li")
|
|
26
25
|
subcategories = []
|
|
27
26
|
for s in subcategory_list:
|
|
28
|
-
a = s.find(
|
|
29
|
-
href = a.get(
|
|
30
|
-
category_id = href.split(
|
|
27
|
+
a = s.find("a")
|
|
28
|
+
href = a.get("href")
|
|
29
|
+
category_id = href.split("/")[-2]
|
|
31
30
|
name = s.get_text()
|
|
32
31
|
subcategories.append(GameSubCategory(id=category_id, sub_name=name))
|
|
33
32
|
title = GameTitle(id=data_id, name=game_name)
|
|
@@ -36,4 +35,4 @@ class CategoryParser(BaseParser):
|
|
|
36
35
|
|
|
37
36
|
@classmethod
|
|
38
37
|
def parse_category_browser(cls, html_content):
|
|
39
|
-
BeautifulSoup(html_content,
|
|
38
|
+
BeautifulSoup(html_content, "html.parser")
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from bs4 import BeautifulSoup
|
|
7
|
+
|
|
8
|
+
from fpx.models.chat import Chat
|
|
9
|
+
from fpx.utils import errors as fpx_err
|
|
10
|
+
|
|
11
|
+
from ._base import BaseParser
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("fpx.chat_parser")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChatParser(BaseParser):
|
|
17
|
+
@classmethod
|
|
18
|
+
def parse_chats_list(cls, html_content: str) -> list[Chat]:
|
|
19
|
+
"""Парсит страницу https://funpay.com/chat/"""
|
|
20
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
21
|
+
items = soup.find_all("a", class_="contact-item")
|
|
22
|
+
if not items:
|
|
23
|
+
items = cls._safe_parse_links(html_content, r"node=\d+")
|
|
24
|
+
if not items:
|
|
25
|
+
raise fpx_err.FpxNullDataError("На странице не найдено ни одного чата")
|
|
26
|
+
chats = []
|
|
27
|
+
for item in items:
|
|
28
|
+
try:
|
|
29
|
+
href = str(item.get("href", ""))
|
|
30
|
+
node_msg_id = int(str(item.get("data-node-msg", "0")))
|
|
31
|
+
chat_id = href.split("node=")[-1] if "node=" in href else ""
|
|
32
|
+
username = cls.clean_text(item.find("div", class_="media-user-name"))
|
|
33
|
+
last_msg = cls.clean_text(item.find("div", class_="contact-item-message"))
|
|
34
|
+
date = cls.clean_text(item.find("div", class_="contact-item-time"))
|
|
35
|
+
is_unread = "unread" in (item.get("class", "") or [])
|
|
36
|
+
chats.append(
|
|
37
|
+
Chat(
|
|
38
|
+
id=chat_id,
|
|
39
|
+
node_msg_id=node_msg_id,
|
|
40
|
+
username=username,
|
|
41
|
+
last_msg=last_msg,
|
|
42
|
+
date=date,
|
|
43
|
+
link=href,
|
|
44
|
+
is_unread=is_unread,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.debug(f"Ошибка парсинга отдельного чата: {e}. Пропускаем элемент.")
|
|
49
|
+
continue
|
|
50
|
+
if not chats:
|
|
51
|
+
raise fpx_err.FpxParseError(
|
|
52
|
+
"Не удалось распарсить ни один чат, верстка полностью изменилась, или что-то сломалось."
|
|
53
|
+
)
|
|
54
|
+
return chats
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def parse_chat(cls, html_content: str):
|
|
58
|
+
"""Парсит страницу https://funpay.com/chat/?node=..."""
|
|
59
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
60
|
+
result: dict[str, Any] = {}
|
|
61
|
+
chat_div = soup.find("div", class_="chat")
|
|
62
|
+
if not chat_div:
|
|
63
|
+
chat_div = soup.find("div", attrs={"data-id": re.compile(r"^\d+$")})
|
|
64
|
+
body = soup.find("body")
|
|
65
|
+
if not chat_div or not body:
|
|
66
|
+
raise fpx_err.FpxNullDataError("На странице чата не найден блок переписки или тег body")
|
|
67
|
+
chats = soup.find_all("div", class_="chat-msg-item")
|
|
68
|
+
result["messages"] = []
|
|
69
|
+
if chats:
|
|
70
|
+
for chat in chats:
|
|
71
|
+
try:
|
|
72
|
+
res: dict[str, Any] = {"is_system": False, "node_id": chat.get("id").split("-")[-1]}
|
|
73
|
+
msg_tag = chat.find("div", class_="chat-msg-text")
|
|
74
|
+
if msg_tag:
|
|
75
|
+
message = msg_tag.get_text(separator="\n").strip() if msg_tag else ""
|
|
76
|
+
if not message:
|
|
77
|
+
img_link = msg_tag.find("a", class_="chat-img-link")
|
|
78
|
+
message = img_link.get("href", "") if img_link else "" # type: ignore[assignment]
|
|
79
|
+
res["message"] = message
|
|
80
|
+
else:
|
|
81
|
+
res["message"] = ""
|
|
82
|
+
author_block = None
|
|
83
|
+
current_node = chat
|
|
84
|
+
while current_node:
|
|
85
|
+
author_block = current_node.find("div", class_="media-user-name")
|
|
86
|
+
if author_block:
|
|
87
|
+
break
|
|
88
|
+
current_node = current_node.find_previous_sibling("div", class_="chat-msg-item") # type: ignore[assignment]
|
|
89
|
+
if author_block:
|
|
90
|
+
author = author_block.find("a", class_="chat-msg-author-link")
|
|
91
|
+
if not author:
|
|
92
|
+
sender_lbl = author_block.find("span", class_="chat-msg-author-label")
|
|
93
|
+
res["sender"] = cls.clean_text(sender_lbl) if sender_lbl else "FunPay"
|
|
94
|
+
if res["sender"] and res["sender"].lower() == "оповещение":
|
|
95
|
+
res["sender"] = "FunPay" # type: ignore[assignment]
|
|
96
|
+
res["is_system"] = True
|
|
97
|
+
else:
|
|
98
|
+
res["sender"] = author.get_text(strip=True) # type: ignore[assignment]
|
|
99
|
+
else:
|
|
100
|
+
res["sender"] = "Unknown" # type: ignore[assignment]
|
|
101
|
+
result["messages"].append(res)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.debug(f"Не удалось распарсить N сообщение в чате: {e}")
|
|
104
|
+
else:
|
|
105
|
+
logger.debug("Сообщений не найдено!")
|
|
106
|
+
# парсинг тех.данных
|
|
107
|
+
try:
|
|
108
|
+
result["data-name"] = chat_div.get("data-name", "") # type: ignore[assignment]
|
|
109
|
+
app_data_str = str(body.get("data-app-data", "{}") or "{}")
|
|
110
|
+
app_data = json.loads(app_data_str)
|
|
111
|
+
result["csrf-token"] = app_data.get("csrf-token", "")
|
|
112
|
+
result["user-id"] = app_data.get("userId", "")
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.debug(f"Ошибка извлечения системных данных чата: {e}")
|
|
115
|
+
raise fpx_err.FpxParseError("Не удалось распарсить системные метаданные чата (CSRF/User ID)")
|
|
116
|
+
return result
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
|
|
5
|
+
from fpx.utils import errors as fpx_err
|
|
6
|
+
|
|
7
|
+
from ._base import BaseParser
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("fpx.lot_parser")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LotParser(BaseParser):
|
|
13
|
+
@classmethod
|
|
14
|
+
def parse_lot_menu(cls, html_content: str):
|
|
15
|
+
"""Парсит страницу pay.com/lots/.../trade"""
|
|
16
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
17
|
+
button = soup.find("button", class_="js-lot-raise")
|
|
18
|
+
if not button:
|
|
19
|
+
button = soup.find("button", attrs={"data-game": True})
|
|
20
|
+
if button:
|
|
21
|
+
data_game = button.get("data-game")
|
|
22
|
+
if data_game:
|
|
23
|
+
return data_game
|
|
24
|
+
raise fpx_err.FpxParseError("Атрибут data-game не найден внутри кнопки поднятия")
|
|
25
|
+
raise fpx_err.FpxNullDataError(
|
|
26
|
+
"На странице лотов не найдена кнопка для поднятия.Возможно у вас нет созданных лотов в этой категории"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def parse_current_lot_menu(cls, html_content):
|
|
31
|
+
"""https://funpay.com/lots/offer?id=..."""
|
|
32
|
+
result = {}
|
|
33
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
34
|
+
param_items = soup.find_all("div", class_="param-item")
|
|
35
|
+
if not param_items:
|
|
36
|
+
param_items = [h5.find_parent("div") for h5 in soup.find_all("h5") if h5.find_parent("div")]
|
|
37
|
+
if not param_items:
|
|
38
|
+
raise fpx_err.FpxNullDataError(
|
|
39
|
+
"Страница лота не найдена. Возможно, указана инвалидная ссылка или лот был удалён."
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
descriptions = {}
|
|
43
|
+
for item in param_items:
|
|
44
|
+
try:
|
|
45
|
+
header_tag = item.find(["h5", "h4", "label"])
|
|
46
|
+
content_tag = item.find("div") or item.find("textarea")
|
|
47
|
+
if header_tag and content_tag:
|
|
48
|
+
header = header_tag.get_text(strip=True)
|
|
49
|
+
text = content_tag.get_text(separator="\n", strip=True)
|
|
50
|
+
descriptions[header] = text
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.debug(f"При парсинге конкретного объекта произошла ошибка: {e}")
|
|
53
|
+
result["short_desc"] = (
|
|
54
|
+
descriptions.get("Краткое описание")
|
|
55
|
+
or descriptions.get("Short description")
|
|
56
|
+
or descriptions.get("Короткий опис")
|
|
57
|
+
)
|
|
58
|
+
result["description"] = (
|
|
59
|
+
descriptions.get("Подробное описание")
|
|
60
|
+
or descriptions.get("Detailed description")
|
|
61
|
+
or descriptions.get("Докладний опис")
|
|
62
|
+
)
|
|
63
|
+
option = soup.find("option", value="21") or soup.find("option", attrs={"data-content": True})
|
|
64
|
+
if option:
|
|
65
|
+
inner_html = option.get("data-content")
|
|
66
|
+
if inner_html:
|
|
67
|
+
inner_soup = BeautifulSoup(inner_html, "html.parser")
|
|
68
|
+
price_span = inner_soup.find("span", class_="payment-value")
|
|
69
|
+
raw_price = price_span.get_text(strip=True) if price_span else inner_soup.get_text(strip=True)
|
|
70
|
+
raw_price = raw_price.replace(",", ".")
|
|
71
|
+
cleaned_price = "".join(c for c in raw_price if c.isdigit() or c == ".")
|
|
72
|
+
try:
|
|
73
|
+
result["price"] = float(cleaned_price) if cleaned_price else 0.0
|
|
74
|
+
except ValueError:
|
|
75
|
+
result["price"] = 0.0
|
|
76
|
+
return result
|
|
77
|
+
raise fpx_err.FpxNullDataError("Не удалось найти цену в скрытых атрибутах выбора оплаты.")
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def parse_edit_lot_page(cls, html_content):
|
|
81
|
+
"""https://funpay.com/lots/offerEdit?node=...&offer=..."""
|
|
82
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
83
|
+
result = {}
|
|
84
|
+
hidden_inputs = soup.find_all("input", type="hidden")
|
|
85
|
+
if not hidden_inputs:
|
|
86
|
+
raise fpx_err.FpxNullDataError("Не найдено вводных данных в редакторе лота. Возможно слетела сессия")
|
|
87
|
+
result = {tag.get("name"): tag.get("value", "") for tag in hidden_inputs if tag.get("name")}
|
|
88
|
+
selects = soup.find_all("select")
|
|
89
|
+
if not selects:
|
|
90
|
+
raise fpx_err.FpxNullDataError(
|
|
91
|
+
"Ни одна выборка в редакторе лотов не найдена. Проверьте актуальность сессии"
|
|
92
|
+
)
|
|
93
|
+
for s in selects:
|
|
94
|
+
try:
|
|
95
|
+
name = s.get("name")
|
|
96
|
+
if not name:
|
|
97
|
+
continue
|
|
98
|
+
selected_option = s.find("option", selected=True)
|
|
99
|
+
if selected_option:
|
|
100
|
+
result[name] = selected_option.get("value", "")
|
|
101
|
+
else:
|
|
102
|
+
first_opt = s.find("option")
|
|
103
|
+
result[name] = first_opt.get("value", "") if first_opt else ""
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.debug(f"При парсинге конкретной выборки произошла ошибка: {e}")
|
|
106
|
+
inputs = soup.find_all("input", class_="form-control")
|
|
107
|
+
if not inputs:
|
|
108
|
+
inputs = [
|
|
109
|
+
i
|
|
110
|
+
for i in soup.find_all("input")
|
|
111
|
+
if i.get("name") and i.get("type") in ["text", "number", None] and i.get("type") != "hidden"
|
|
112
|
+
]
|
|
113
|
+
if not inputs:
|
|
114
|
+
logger.debug("Ни одно поле для ввода в редакторе лотов не найдено. Возможно всё в порядке")
|
|
115
|
+
else:
|
|
116
|
+
for i in inputs:
|
|
117
|
+
try:
|
|
118
|
+
name = i.get("name")
|
|
119
|
+
if name:
|
|
120
|
+
result[name] = i.get("value", "")
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.debug(f"При парсинге конкретного поля для ввода произошла ошибка: {e}")
|
|
123
|
+
textareas = soup.find_all("textarea")
|
|
124
|
+
if not textareas:
|
|
125
|
+
logger.debug("При парсинге редактора лотов не найдено ни одного текстового поля. Возможно всё в порядке")
|
|
126
|
+
else:
|
|
127
|
+
for t in textareas:
|
|
128
|
+
try:
|
|
129
|
+
name = t.get("name")
|
|
130
|
+
if name:
|
|
131
|
+
result[name] = t.get_text().strip()
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.debug(f"При парсинге конкретного текстового поля произошла ошибка: {e}")
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def parse_create_lot_page(cls, html_content):
|
|
138
|
+
"""https://funpay.com/lots/offerEdit?node=..."""
|
|
139
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
140
|
+
result = {}
|
|
141
|
+
hidden_inputs = soup.find_all("input", type="hidden")
|
|
142
|
+
if not hidden_inputs:
|
|
143
|
+
raise fpx_err.FpxNullDataError("Не найдено вводных данных в редакторе лота. Возможно слетела сессия")
|
|
144
|
+
result = {tag.get("name"): tag.get("value", "") for tag in hidden_inputs if tag.get("name")}
|
|
145
|
+
selects = soup.find_all("select")
|
|
146
|
+
if not selects:
|
|
147
|
+
raise fpx_err.FpxNullDataError(
|
|
148
|
+
"Ни одна выборка в редакторе лотов не найдена. Проверьте актуальность сессии"
|
|
149
|
+
)
|
|
150
|
+
for s in selects:
|
|
151
|
+
try:
|
|
152
|
+
name = s.get("name", "")
|
|
153
|
+
options = s.find_all("option")
|
|
154
|
+
value_list = []
|
|
155
|
+
for option in options:
|
|
156
|
+
value_name = option.get_text(strip=True)
|
|
157
|
+
value = option.get("value", "")
|
|
158
|
+
if value_name and value:
|
|
159
|
+
value_list.append({value_name: value})
|
|
160
|
+
result[name] = value_list
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.debug(f"При парсинге конкретной выборки произошла ошибка: {e}")
|
|
163
|
+
inputs = soup.find_all("input", class_="form-control")
|
|
164
|
+
if not inputs:
|
|
165
|
+
inputs = [
|
|
166
|
+
i
|
|
167
|
+
for i in soup.find_all("input")
|
|
168
|
+
if i.get("name") and i.get("type") in ["text", "number", None] and i.get("type") != "hidden"
|
|
169
|
+
]
|
|
170
|
+
if not inputs:
|
|
171
|
+
logger.debug("Ни одно поле для ввода в редакторе лотов не найдено. Возможно всё в порядке")
|
|
172
|
+
else:
|
|
173
|
+
for i in inputs:
|
|
174
|
+
try:
|
|
175
|
+
name = i.get("name")
|
|
176
|
+
if name:
|
|
177
|
+
result[name] = i.get("value", "")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.debug(f"При парсинге конкретного поля для ввода произошла ошибка: {e}")
|
|
180
|
+
textareas = soup.find_all("textarea")
|
|
181
|
+
if not textareas:
|
|
182
|
+
logger.debug("При парсинге редактора лотов не найдено ни одного текстового поля. Возможно всё в порядке")
|
|
183
|
+
else:
|
|
184
|
+
for t in textareas:
|
|
185
|
+
try:
|
|
186
|
+
name = t.get("name")
|
|
187
|
+
if name:
|
|
188
|
+
result[name] = t.get_text().strip()
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.debug(f"При парсинге конкретного текстового поля произошла ошибка: {e}")
|
|
191
|
+
return result
|