arizona-forum-api-async 1.0__py3-none-any.whl
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.
- arizona_forum_api_async-1.0.dist-info/METADATA +91 -0
- arizona_forum_api_async-1.0.dist-info/RECORD +17 -0
- arizona_forum_api_async-1.0.dist-info/WHEEL +5 -0
- arizona_forum_api_async-1.0.dist-info/licenses/LICENSE +21 -0
- arizona_forum_api_async-1.0.dist-info/top_level.txt +1 -0
- arizona_forum_async/__init__.py +3 -0
- arizona_forum_async/api.py +1449 -0
- arizona_forum_async/bypass_antibot/__init__.py +1 -0
- arizona_forum_async/bypass_antibot/script.py +883 -0
- arizona_forum_async/consts.py +24 -0
- arizona_forum_async/exceptions.py +18 -0
- arizona_forum_async/models/__init__.py +4 -0
- arizona_forum_async/models/category_object.py +111 -0
- arizona_forum_async/models/member_object.py +125 -0
- arizona_forum_async/models/other.py +14 -0
- arizona_forum_async/models/post_object.py +152 -0
- arizona_forum_async/models/thread_object.py +140 -0
@@ -0,0 +1,1449 @@
|
|
1
|
+
import aiohttp
|
2
|
+
from bs4 import BeautifulSoup
|
3
|
+
from re import compile, findall
|
4
|
+
import re
|
5
|
+
from html import unescape
|
6
|
+
from typing import List, Dict, Optional, Union
|
7
|
+
|
8
|
+
from arizona_forum_async.consts import MAIN_URL, ROLE_COLOR
|
9
|
+
from arizona_forum_async.bypass_antibot import bypass_async
|
10
|
+
|
11
|
+
from arizona_forum_async.exceptions import IncorrectLoginData, ThisIsYouError
|
12
|
+
from arizona_forum_async.models.other import Statistic
|
13
|
+
from arizona_forum_async.models.post_object import Post, ProfilePost
|
14
|
+
from arizona_forum_async.models.member_object import Member, CurrentMember
|
15
|
+
from arizona_forum_async.models.thread_object import Thread
|
16
|
+
from arizona_forum_async.models.category_object import Category
|
17
|
+
|
18
|
+
|
19
|
+
class ArizonaAPI:
|
20
|
+
def __init__(self, user_agent: str, cookie: dict) -> None:
|
21
|
+
self.user_agent = user_agent
|
22
|
+
self.cookie_str = "; ".join([f"{k}={v}" for k, v in cookie.items()])
|
23
|
+
self._session: aiohttp.ClientSession = None
|
24
|
+
self._token: str = None
|
25
|
+
|
26
|
+
|
27
|
+
async def connect(self, do_bypass: bool = True):
|
28
|
+
"""Асинхронный метод для создания сессии, получения токена и обхода анти-бота."""
|
29
|
+
if self._session is None or self._session.closed:
|
30
|
+
cookies = {}
|
31
|
+
for item in self.cookie_str.split('; '):
|
32
|
+
name, value = item.strip().split('=', 1)
|
33
|
+
cookies[name] = value
|
34
|
+
|
35
|
+
if do_bypass:
|
36
|
+
bypass_cookie_str, _ = await bypass_async(self.user_agent)
|
37
|
+
name, value = bypass_cookie_str.split('=', 1)
|
38
|
+
cookies[name] = value
|
39
|
+
|
40
|
+
self._session = aiohttp.ClientSession(
|
41
|
+
headers={"user-agent": self.user_agent},
|
42
|
+
cookies=cookies
|
43
|
+
)
|
44
|
+
|
45
|
+
try:
|
46
|
+
async with self._session.get(f"{MAIN_URL}/account/") as response:
|
47
|
+
response.raise_for_status()
|
48
|
+
html_content_main = await response.text()
|
49
|
+
soup_main = BeautifulSoup(html_content_main, 'lxml')
|
50
|
+
html_tag = soup_main.find('html')
|
51
|
+
if not html_tag or html_tag.get('data-logged-in') == "false":
|
52
|
+
raise IncorrectLoginData("Неверные cookie или сессия истекла.")
|
53
|
+
|
54
|
+
async with self._session.get(f"{MAIN_URL}/help/terms/") as response:
|
55
|
+
response.raise_for_status()
|
56
|
+
html_content = await response.text()
|
57
|
+
soup = BeautifulSoup(html_content, 'lxml')
|
58
|
+
self._token = soup.find('html')['data-csrf']
|
59
|
+
if not self._token:
|
60
|
+
raise Exception("Не удалось получить CSRF токен.")
|
61
|
+
|
62
|
+
except (aiohttp.ClientError, IncorrectLoginData, Exception) as e:
|
63
|
+
if self._session:
|
64
|
+
await self._session.close()
|
65
|
+
self._session = None
|
66
|
+
raise Exception(f"Ошибка подключения или авторизации: {e}") from e
|
67
|
+
|
68
|
+
async def close(self):
|
69
|
+
"""Асинхронный метод для закрытия сессии."""
|
70
|
+
if self._session and not self._session.closed:
|
71
|
+
await self._session.close()
|
72
|
+
|
73
|
+
@property
|
74
|
+
async def token(self) -> str:
|
75
|
+
if not self._session or self._session.closed:
|
76
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
77
|
+
if not self._token:
|
78
|
+
async with self._session.get(f"{MAIN_URL}/help/terms/") as response:
|
79
|
+
response.raise_for_status()
|
80
|
+
html_content = await response.text()
|
81
|
+
soup = BeautifulSoup(html_content, 'lxml')
|
82
|
+
self._token = soup.find('html')['data-csrf']
|
83
|
+
return self._token
|
84
|
+
|
85
|
+
async def get_current_member(self) -> CurrentMember:
|
86
|
+
if not self._session or self._session.closed:
|
87
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
88
|
+
try:
|
89
|
+
async with self._session.get(f"{MAIN_URL}/account/") as response:
|
90
|
+
response.raise_for_status()
|
91
|
+
html_content = await response.text()
|
92
|
+
soup = BeautifulSoup(html_content, 'lxml')
|
93
|
+
avatar_span = soup.find('span', {'class': 'avatar--xxs'})
|
94
|
+
if not avatar_span or not avatar_span.has_attr('data-user-id'):
|
95
|
+
raise Exception("Не удалось найти ID текущего пользователя на странице аккаунта.")
|
96
|
+
user_id = int(avatar_span['data-user-id'])
|
97
|
+
|
98
|
+
member_info = await self.get_member(user_id)
|
99
|
+
if not member_info:
|
100
|
+
raise Exception(f"Не удалось получить информацию для пользователя с ID {user_id}")
|
101
|
+
|
102
|
+
return CurrentMember(self, user_id, member_info.username, member_info.user_title,
|
103
|
+
member_info.avatar, member_info.roles, member_info.messages_count,
|
104
|
+
member_info.reactions_count, member_info.trophies_count, member_info.username_color)
|
105
|
+
except aiohttp.ClientError as e:
|
106
|
+
print(f"Ошибка сети при получении данных текущего пользователя: {e}")
|
107
|
+
return None
|
108
|
+
except Exception as e:
|
109
|
+
print(f"Неожиданная ошибка при получении данных текущего пользователя: {e}")
|
110
|
+
return None
|
111
|
+
|
112
|
+
async def get_category(self, category_id: int) -> 'Category | None':
|
113
|
+
if not self._session or self._session.closed:
|
114
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
115
|
+
token = await self.token
|
116
|
+
url = f"{MAIN_URL}/forums/{category_id}"
|
117
|
+
params = {'_xfResponseType': 'json', '_xfToken': token}
|
118
|
+
try:
|
119
|
+
async with self._session.get(url, params=params) as response:
|
120
|
+
response.raise_for_status()
|
121
|
+
data = await response.json()
|
122
|
+
|
123
|
+
if data.get('status') == 'error':
|
124
|
+
return None
|
125
|
+
|
126
|
+
html_content = unescape(data['html']['content'])
|
127
|
+
soup = BeautifulSoup(html_content, 'lxml')
|
128
|
+
title = unescape(data['html']['title'])
|
129
|
+
try:
|
130
|
+
pages_count = int(soup.find_all('li', {'class': 'pageNav-page'})[-1].text)
|
131
|
+
except (IndexError, AttributeError, ValueError):
|
132
|
+
pages_count = 1
|
133
|
+
|
134
|
+
return Category(self, category_id, title, pages_count)
|
135
|
+
except aiohttp.ClientError as e:
|
136
|
+
print(f"Ошибка сети при получении категории {category_id}: {e}")
|
137
|
+
return None
|
138
|
+
except Exception as e:
|
139
|
+
print(f"Неожиданная ошибка при получении категории {category_id}: {e}")
|
140
|
+
return None
|
141
|
+
|
142
|
+
|
143
|
+
async def get_member(self, user_id: int) -> 'Member | None':
|
144
|
+
if not self._session or self._session.closed:
|
145
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
146
|
+
token = await self.token
|
147
|
+
url = f"{MAIN_URL}/members/{user_id}"
|
148
|
+
params = {'_xfResponseType': 'json', '_xfToken': token}
|
149
|
+
try:
|
150
|
+
async with self._session.get(url, params=params) as response:
|
151
|
+
response.raise_for_status()
|
152
|
+
data = await response.json()
|
153
|
+
|
154
|
+
if data.get('status') == 'error':
|
155
|
+
return None
|
156
|
+
|
157
|
+
html_content = unescape(data['html']['content'])
|
158
|
+
soup = BeautifulSoup(html_content, 'lxml')
|
159
|
+
username = unescape(data['html']['title'])
|
160
|
+
|
161
|
+
username_class = soup.find('span', class_='username')
|
162
|
+
username_color = '#fff'
|
163
|
+
if username_class:
|
164
|
+
for style in ROLE_COLOR:
|
165
|
+
if style in str(username_class):
|
166
|
+
username_color = ROLE_COLOR[style]
|
167
|
+
break
|
168
|
+
|
169
|
+
roles = []
|
170
|
+
roles_container = soup.find('div', {'class': 'memberHeader-banners'})
|
171
|
+
if roles_container:
|
172
|
+
for i in roles_container.children:
|
173
|
+
if i.text != '\n': roles.append(i.text.strip()) # Added strip()
|
174
|
+
|
175
|
+
try:
|
176
|
+
user_title_tag = soup.find('span', {'class': 'userTitle'})
|
177
|
+
user_title = user_title_tag.text if user_title_tag else None
|
178
|
+
except AttributeError:
|
179
|
+
user_title = None
|
180
|
+
|
181
|
+
try:
|
182
|
+
avatar_tag = soup.find('a', {'class': 'avatar avatar--l'})
|
183
|
+
avatar = MAIN_URL + avatar_tag['href'] if avatar_tag and avatar_tag.has_attr('href') else None
|
184
|
+
except TypeError:
|
185
|
+
avatar = None
|
186
|
+
|
187
|
+
messages_count = 0
|
188
|
+
reactions_count = 0
|
189
|
+
trophies_count = 0
|
190
|
+
|
191
|
+
try:
|
192
|
+
msg_tag = soup.find('a', {'href': f'/search/member?user_id={user_id}'})
|
193
|
+
if msg_tag: messages_count = int(msg_tag.text.strip().replace(',', ''))
|
194
|
+
except (AttributeError, ValueError): pass
|
195
|
+
|
196
|
+
try:
|
197
|
+
react_tag = soup.find('dl', {'class': 'pairs pairs--rows pairs--rows--centered'})
|
198
|
+
if react_tag:
|
199
|
+
dd_tag = react_tag.find('dd')
|
200
|
+
if dd_tag: reactions_count = int(dd_tag.text.strip().replace(',', ''))
|
201
|
+
except (AttributeError, ValueError): pass
|
202
|
+
|
203
|
+
try:
|
204
|
+
trophy_tag = soup.find('a', {'href': f'/members/{user_id}/trophies'})
|
205
|
+
if trophy_tag: trophies_count = int(trophy_tag.text.strip().replace(',', ''))
|
206
|
+
except (AttributeError, ValueError): pass
|
207
|
+
|
208
|
+
return Member(self, user_id, username, user_title, avatar, roles, messages_count, reactions_count, trophies_count, username_color)
|
209
|
+
|
210
|
+
except aiohttp.ClientError as e:
|
211
|
+
print(f"Ошибка сети при получении пользователя {user_id}: {e}")
|
212
|
+
return None
|
213
|
+
except Exception as e:
|
214
|
+
print(f"Неожиданная ошибка при получении пользователя {user_id}: {e}")
|
215
|
+
return None
|
216
|
+
|
217
|
+
|
218
|
+
async def get_thread(self, thread_id: int) -> 'Thread | None':
|
219
|
+
if not self._session or self._session.closed:
|
220
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
221
|
+
token = await self.token
|
222
|
+
url = f"{MAIN_URL}/threads/{thread_id}/page-1"
|
223
|
+
params = {'_xfResponseType': 'json', '_xfToken': token}
|
224
|
+
try:
|
225
|
+
async with self._session.get(url, params=params) as response:
|
226
|
+
response.raise_for_status()
|
227
|
+
data = await response.json()
|
228
|
+
|
229
|
+
if data.get('status') == 'error':
|
230
|
+
return None
|
231
|
+
|
232
|
+
if data.get('redirect'):
|
233
|
+
try:
|
234
|
+
redirect_path = data['redirect'].strip(MAIN_URL)
|
235
|
+
new_thread_id = int(redirect_path.split('/')[1].split('-')[-1])
|
236
|
+
return await self.get_thread(new_thread_id)
|
237
|
+
except (IndexError, ValueError):
|
238
|
+
print(f"Не удалось извлечь thread_id из редиректа: {data['redirect']}")
|
239
|
+
return None
|
240
|
+
|
241
|
+
html_content = unescape(data['html']['content'])
|
242
|
+
content_h1_html = unescape(data['html']['h1'])
|
243
|
+
content_soup = BeautifulSoup(html_content, 'lxml')
|
244
|
+
content_h1_soup = BeautifulSoup(content_h1_html, 'lxml')
|
245
|
+
|
246
|
+
creator = None
|
247
|
+
creator_tag = content_soup.find('a', {'class': 'username'})
|
248
|
+
if creator_tag and creator_tag.has_attr('data-user-id'):
|
249
|
+
creator_id = int(creator_tag['data-user-id'])
|
250
|
+
try:
|
251
|
+
creator = await self.get_member(creator_id)
|
252
|
+
except Exception as e:
|
253
|
+
print(f"Ошибка получения создателя ({creator_id}) для темы {thread_id}: {e}")
|
254
|
+
if not creator:
|
255
|
+
creator = Member(self, creator_id, creator_tag.text, None, None, None, None, None, None, None)
|
256
|
+
else:
|
257
|
+
print(f"Не удалось найти информацию о создателе для темы {thread_id}")
|
258
|
+
return None
|
259
|
+
|
260
|
+
|
261
|
+
create_date_tag = content_soup.find('time')
|
262
|
+
create_date = int(create_date_tag['data-time']) if create_date_tag and create_date_tag.has_attr('data-time') else 0
|
263
|
+
|
264
|
+
prefix_tag = content_h1_soup.find('span', {'class': 'label'})
|
265
|
+
if prefix_tag:
|
266
|
+
prefix = prefix_tag.text
|
267
|
+
title = content_h1_soup.text.strip(prefix).strip()
|
268
|
+
else:
|
269
|
+
prefix = ""
|
270
|
+
title = content_h1_soup.text.strip()
|
271
|
+
|
272
|
+
thread_html_content_tag = content_soup.find('div', {'class': 'bbWrapper'})
|
273
|
+
thread_html_content = str(thread_html_content_tag) if thread_html_content_tag else ""
|
274
|
+
thread_content = thread_html_content_tag.text if thread_html_content_tag else ""
|
275
|
+
|
276
|
+
try:
|
277
|
+
pages_count = int(content_soup.find_all('li', {'class': 'pageNav-page'})[-1].text)
|
278
|
+
except (IndexError, AttributeError, ValueError):
|
279
|
+
pages_count = 1
|
280
|
+
|
281
|
+
is_closed = bool(content_soup.find('dl', {'class': 'blockStatus'}))
|
282
|
+
|
283
|
+
post_article_tag = content_soup.find('article', {'id': compile(r'js-post-\d+')})
|
284
|
+
thread_post_id = int(post_article_tag['id'].strip('js-post-')) if post_article_tag and post_article_tag.has_attr('id') else 0
|
285
|
+
|
286
|
+
return Thread(self, thread_id, creator, create_date, title, prefix, thread_content, thread_html_content, pages_count, thread_post_id, is_closed)
|
287
|
+
|
288
|
+
except aiohttp.ClientError as e:
|
289
|
+
print(f"Ошибка сети при получении темы {thread_id}: {e}")
|
290
|
+
return None
|
291
|
+
except Exception as e:
|
292
|
+
print(f"Неожиданная ошибка при получении темы {thread_id}: {e}")
|
293
|
+
return None
|
294
|
+
|
295
|
+
|
296
|
+
async def get_post(self, post_id: int) -> 'Post | None':
|
297
|
+
if not self._session or self._session.closed:
|
298
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
299
|
+
url = f"{MAIN_URL}/posts/{post_id}"
|
300
|
+
try:
|
301
|
+
async with self._session.get(url) as response:
|
302
|
+
response.raise_for_status()
|
303
|
+
html_content = await response.text()
|
304
|
+
|
305
|
+
content_soup = BeautifulSoup(html_content, 'lxml')
|
306
|
+
post_article = content_soup.find('article', {'id': f'js-post-{post_id}'})
|
307
|
+
if post_article is None:
|
308
|
+
return None
|
309
|
+
|
310
|
+
creator = None
|
311
|
+
creator_info_tag = post_article.find('a', {'data-xf-init': 'member-tooltip'})
|
312
|
+
if creator_info_tag and creator_info_tag.has_attr('data-user-id'):
|
313
|
+
creator_id = int(creator_info_tag['data-user-id'])
|
314
|
+
try:
|
315
|
+
creator = await self.get_member(creator_id)
|
316
|
+
except Exception as e:
|
317
|
+
print(f"Ошибка получения создателя ({creator_id}) для поста {post_id}: {e}")
|
318
|
+
if not creator:
|
319
|
+
creator = Member(self, creator_id, creator_info_tag.text, None, None, None, None, None, None, None)
|
320
|
+
else:
|
321
|
+
print(f"Не удалось найти информацию о создателе для поста {post_id}")
|
322
|
+
return None
|
323
|
+
|
324
|
+
thread = None
|
325
|
+
html_tag = content_soup.find('html')
|
326
|
+
if html_tag and html_tag.has_attr('data-content-key') and html_tag['data-content-key'].startswith('thread-'):
|
327
|
+
try:
|
328
|
+
thread_id = int(html_tag['data-content-key'].strip('thread-'))
|
329
|
+
thread = await self.get_thread(thread_id)
|
330
|
+
except (ValueError, Exception) as e:
|
331
|
+
print(f"Ошибка получения темы для поста {post_id}: {e}")
|
332
|
+
if not thread:
|
333
|
+
print(f"Не удалось получить информацию о теме для поста {post_id}")
|
334
|
+
return None
|
335
|
+
|
336
|
+
create_date_tag = post_article.find('time', {'class': 'u-dt'})
|
337
|
+
create_date = int(create_date_tag['data-time']) if create_date_tag and create_date_tag.has_attr('data-time') else 0
|
338
|
+
|
339
|
+
html_content_tag = post_article.find('div', {'class': 'bbWrapper'})
|
340
|
+
html_content = str(html_content_tag) if html_content_tag else ""
|
341
|
+
text_content = html_content_tag.text if html_content_tag else ""
|
342
|
+
|
343
|
+
return Post(self, post_id, creator, thread, create_date, html_content, text_content)
|
344
|
+
|
345
|
+
except aiohttp.ClientError as e:
|
346
|
+
print(f"Ошибка сети при получении поста {post_id}: {e}")
|
347
|
+
return None
|
348
|
+
except Exception as e:
|
349
|
+
print(f"Неожиданная ошибка при получении поста {post_id}: {e}")
|
350
|
+
return None
|
351
|
+
|
352
|
+
|
353
|
+
async def get_profile_post(self, post_id: int) -> 'ProfilePost | None':
|
354
|
+
if not self._session or self._session.closed:
|
355
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
356
|
+
url = f"{MAIN_URL}/profile-posts/{post_id}"
|
357
|
+
try:
|
358
|
+
async with self._session.get(url) as response:
|
359
|
+
response.raise_for_status()
|
360
|
+
html_content = await response.text()
|
361
|
+
|
362
|
+
content_soup = BeautifulSoup(html_content, 'lxml')
|
363
|
+
post_article = content_soup.find('article', {'id': f'js-profilePost-{post_id}'})
|
364
|
+
if post_article is None:
|
365
|
+
return None
|
366
|
+
|
367
|
+
creator = None
|
368
|
+
creator_tag = post_article.find('a', {'class': 'username'})
|
369
|
+
if creator_tag and creator_tag.has_attr('data-user-id'):
|
370
|
+
creator_id = int(creator_tag['data-user-id'])
|
371
|
+
creator = await self.get_member(creator_id)
|
372
|
+
if not creator:
|
373
|
+
print(f"Не удалось получить создателя ({creator_id}) для поста профиля {post_id}")
|
374
|
+
return None
|
375
|
+
else:
|
376
|
+
print(f"Не удалось найти ID создателя для поста профиля {post_id}")
|
377
|
+
return None
|
378
|
+
|
379
|
+
profile_owner = None
|
380
|
+
profile_owner_tag = post_article.find('h4', {'class': 'attribution'})
|
381
|
+
if profile_owner_tag:
|
382
|
+
profile_link = profile_owner_tag.find('a', {'class': 'username'})
|
383
|
+
if profile_link and profile_link.has_attr('data-user-id'):
|
384
|
+
try:
|
385
|
+
profile_id = int(profile_link['data-user-id'])
|
386
|
+
profile_owner = await self.get_member(profile_id)
|
387
|
+
except (ValueError, Exception) as e:
|
388
|
+
print(f"Ошибка получения владельца профиля ({profile_id}) для поста {post_id}: {e}")
|
389
|
+
|
390
|
+
if not profile_owner:
|
391
|
+
print(f"Не удалось определить владельца профиля для поста {post_id}")
|
392
|
+
return None
|
393
|
+
|
394
|
+
create_date_tag = post_article.find('time')
|
395
|
+
create_date = int(create_date_tag['data-time']) if create_date_tag and create_date_tag.has_attr('data-time') else 0
|
396
|
+
|
397
|
+
html_content_tag = post_article.find('div', {'class': 'bbWrapper'})
|
398
|
+
html_content = str(html_content_tag) if html_content_tag else ""
|
399
|
+
text_content = html_content_tag.text if html_content_tag else ""
|
400
|
+
|
401
|
+
return ProfilePost(self, post_id, creator, profile_owner, create_date, html_content, text_content)
|
402
|
+
|
403
|
+
except aiohttp.ClientError as e:
|
404
|
+
print(f"Ошибка сети при получении поста профиля {post_id}: {e}")
|
405
|
+
return None
|
406
|
+
except Exception as e:
|
407
|
+
print(f"Неожиданная ошибка при получении поста профиля {post_id}: {e}")
|
408
|
+
return None
|
409
|
+
|
410
|
+
async def get_forum_statistic(self) -> 'Statistic | None':
|
411
|
+
if not self._session or self._session.closed:
|
412
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
413
|
+
url = MAIN_URL
|
414
|
+
try:
|
415
|
+
async with self._session.get(url) as response:
|
416
|
+
response.raise_for_status()
|
417
|
+
html_content = await response.text()
|
418
|
+
|
419
|
+
content_soup = BeautifulSoup(html_content, 'lxml')
|
420
|
+
|
421
|
+
threads_count = 0
|
422
|
+
posts_count = 0
|
423
|
+
users_count = 0
|
424
|
+
last_register_member = None
|
425
|
+
|
426
|
+
try:
|
427
|
+
threads_tag = content_soup.find('dl', {'class': 'pairs pairs--justified count--threads'})
|
428
|
+
if threads_tag:
|
429
|
+
dd_tag = threads_tag.find('dd')
|
430
|
+
if dd_tag: threads_count = int(dd_tag.text.replace(',', ''))
|
431
|
+
except (AttributeError, ValueError): pass
|
432
|
+
|
433
|
+
try:
|
434
|
+
posts_tag = content_soup.find('dl', {'class': 'pairs pairs--justified count--messages'})
|
435
|
+
if posts_tag:
|
436
|
+
dd_tag = posts_tag.find('dd')
|
437
|
+
if dd_tag: posts_count = int(dd_tag.text.replace(',', ''))
|
438
|
+
except (AttributeError, ValueError): pass
|
439
|
+
|
440
|
+
try:
|
441
|
+
users_tag = content_soup.find('dl', {'class': 'pairs pairs--justified count--users'})
|
442
|
+
if users_tag:
|
443
|
+
dd_tag = users_tag.find('dd')
|
444
|
+
if dd_tag: users_count = int(dd_tag.text.replace(',', ''))
|
445
|
+
except (AttributeError, ValueError): pass
|
446
|
+
|
447
|
+
try:
|
448
|
+
latest_member_dl = content_soup.find('dl', {'class': 'pairs pairs--justified'})
|
449
|
+
if latest_member_dl:
|
450
|
+
latest_member_link = latest_member_dl.find('a', {'data-user-id': True})
|
451
|
+
if latest_member_link and latest_member_link.has_attr('data-user-id'):
|
452
|
+
last_user_id = int(latest_member_link['data-user-id'])
|
453
|
+
last_register_member = await self.get_member(last_user_id)
|
454
|
+
except (AttributeError, ValueError, Exception) as e:
|
455
|
+
print(f"Ошибка получения последнего зарегистрированного пользователя: {e}")
|
456
|
+
|
457
|
+
|
458
|
+
return Statistic(self, threads_count, posts_count, users_count, last_register_member)
|
459
|
+
|
460
|
+
except aiohttp.ClientError as e:
|
461
|
+
print(f"Ошибка сети при получении статистики форума: {e}")
|
462
|
+
return None
|
463
|
+
except Exception as e:
|
464
|
+
print(f"Неожиданная ошибка при получении статистики форума: {e}")
|
465
|
+
return None
|
466
|
+
|
467
|
+
|
468
|
+
# ---------------================ МЕТОДЫ ОБЪЕКТОВ ====================--------------------
|
469
|
+
|
470
|
+
# CATEGORY
|
471
|
+
async def create_thread(self, category_id: int, title: str, message_html: str, discussion_type: str = 'discussion', watch_thread: bool = True) -> aiohttp.ClientResponse:
|
472
|
+
if not self._session or self._session.closed:
|
473
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
474
|
+
token = await self.token
|
475
|
+
url = f"{MAIN_URL}/forums/{category_id}/post-thread"
|
476
|
+
params = {'inline-mode': '1'}
|
477
|
+
payload = {
|
478
|
+
'_xfToken': token,
|
479
|
+
'title': title,
|
480
|
+
'message_html': message_html,
|
481
|
+
'discussion_type': discussion_type,
|
482
|
+
'watch_thread': int(watch_thread)
|
483
|
+
}
|
484
|
+
try:
|
485
|
+
response = await self._session.post(url, params=params, data=payload)
|
486
|
+
return response
|
487
|
+
except aiohttp.ClientError as e:
|
488
|
+
print(f"Ошибка сети при создании темы в категории {category_id}: {e}")
|
489
|
+
raise e
|
490
|
+
|
491
|
+
|
492
|
+
async def set_read_category(self, category_id: int) -> aiohttp.ClientResponse:
|
493
|
+
if not self._session or self._session.closed:
|
494
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
495
|
+
token = await self.token
|
496
|
+
url = f"{MAIN_URL}/forums/{category_id}/mark-read"
|
497
|
+
payload = {'_xfToken': token}
|
498
|
+
try:
|
499
|
+
response = await self._session.post(url, data=payload)
|
500
|
+
return response
|
501
|
+
except aiohttp.ClientError as e:
|
502
|
+
print(f"Ошибка сети при отметке категории {category_id} как прочитанной: {e}")
|
503
|
+
raise e
|
504
|
+
|
505
|
+
|
506
|
+
async def watch_category(self, category_id: int, notify: str, send_alert: bool = True, send_email: bool = False, stop: bool = False) -> aiohttp.ClientResponse:
|
507
|
+
if not self._session or self._session.closed:
|
508
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
509
|
+
token = await self.token
|
510
|
+
url = f"{MAIN_URL}/forums/{category_id}/watch"
|
511
|
+
if stop:
|
512
|
+
payload = {'_xfToken': token, 'stop': "1"}
|
513
|
+
else:
|
514
|
+
payload = {
|
515
|
+
'_xfToken': token,
|
516
|
+
'send_alert': int(send_alert),
|
517
|
+
'send_email': int(send_email),
|
518
|
+
'notify': notify
|
519
|
+
}
|
520
|
+
try:
|
521
|
+
response = await self._session.post(url, data=payload)
|
522
|
+
return response
|
523
|
+
except aiohttp.ClientError as e:
|
524
|
+
print(f"Ошибка сети при настройке отслеживания категории {category_id}: {e}")
|
525
|
+
raise e
|
526
|
+
|
527
|
+
|
528
|
+
async def get_threads(self, category_id: int, page: int = 1) -> Optional[Dict[str, List[int]]]:
|
529
|
+
if not self._session or self._session.closed:
|
530
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
531
|
+
token = await self.token
|
532
|
+
url = f"{MAIN_URL}/forums/{category_id}/page-{page}"
|
533
|
+
params = {'_xfResponseType': 'json', '_xfToken': token}
|
534
|
+
try:
|
535
|
+
async with self._session.get(url, params=params) as response:
|
536
|
+
response.raise_for_status()
|
537
|
+
data = await response.json()
|
538
|
+
|
539
|
+
if data.get('status') == 'error':
|
540
|
+
return None
|
541
|
+
|
542
|
+
html_content = unescape(data['html']['content'])
|
543
|
+
soup = BeautifulSoup(html_content, "lxml")
|
544
|
+
result = {'pins': [], 'unpins': []}
|
545
|
+
for thread in soup.find_all('div', compile('structItem structItem--thread.*')):
|
546
|
+
link_tags = thread.find_all('div', "structItem-title")[0].find_all("a")
|
547
|
+
if not link_tags: continue
|
548
|
+
link = link_tags[-1]
|
549
|
+
thread_ids = findall(r'\d+', link.get('href', ''))
|
550
|
+
if not thread_ids: continue
|
551
|
+
|
552
|
+
thread_id = int(thread_ids[0])
|
553
|
+
if len(thread.find_all('i', {'title': 'Закреплено'})) > 0:
|
554
|
+
result['pins'].append(thread_id)
|
555
|
+
else:
|
556
|
+
result['unpins'].append(thread_id)
|
557
|
+
return result
|
558
|
+
except aiohttp.ClientError as e:
|
559
|
+
print(f"Ошибка сети при получении тем из категории {category_id} (страница {page}): {e}")
|
560
|
+
return None
|
561
|
+
except Exception as e:
|
562
|
+
print(f"Неожиданная ошибка при получении тем из категории {category_id} (страница {page}): {e}")
|
563
|
+
return None
|
564
|
+
|
565
|
+
|
566
|
+
async def get_thread_category_detail(self, category_id: int, page: int = 1) -> Optional[List[Dict]]:
|
567
|
+
if not self._session or self._session.closed:
|
568
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
569
|
+
token = await self.token
|
570
|
+
url = f"{MAIN_URL}/forums/{category_id}/page-{page}"
|
571
|
+
params = {'_xfResponseType': 'json', '_xfToken': token}
|
572
|
+
try:
|
573
|
+
async with self._session.get(url, params=params) as response:
|
574
|
+
response.raise_for_status()
|
575
|
+
data = await response.json()
|
576
|
+
|
577
|
+
if data.get('status') == 'error':
|
578
|
+
return None
|
579
|
+
|
580
|
+
html_content = unescape(data['html']['content'])
|
581
|
+
soup = BeautifulSoup(html_content, "lxml")
|
582
|
+
result = []
|
583
|
+
seen_thread_ids = set()
|
584
|
+
|
585
|
+
for thread in soup.find_all('div', class_=compile('structItem structItem--thread.*')):
|
586
|
+
title_div = thread.find('div', "structItem-title")
|
587
|
+
if not title_div: continue
|
588
|
+
link_tags = title_div.find_all("a")
|
589
|
+
if not link_tags: continue
|
590
|
+
link = link_tags[-1]
|
591
|
+
|
592
|
+
thread_ids = findall(r'\d+', link.get('href', ''))
|
593
|
+
if not thread_ids: continue
|
594
|
+
thread_id = int(thread_ids[0])
|
595
|
+
|
596
|
+
if thread_id in seen_thread_ids:
|
597
|
+
continue
|
598
|
+
seen_thread_ids.add(thread_id)
|
599
|
+
|
600
|
+
thread_data = {}
|
601
|
+
|
602
|
+
minor_div = thread.find('div', 'structItem-cell--main').find('div', 'structItem-minor')
|
603
|
+
username_author_tag = minor_div.find('ul', 'structItem-parts').find('a', class_='username') if minor_div else None
|
604
|
+
thread_data['username_author'] = username_author_tag.text.strip() if username_author_tag else None
|
605
|
+
thread_data['thread_title'] = link.text.strip()
|
606
|
+
|
607
|
+
prefix_label = title_div.find('span', class_='label')
|
608
|
+
thread_data['prefix'] = prefix_label.text.strip() if prefix_label else None
|
609
|
+
|
610
|
+
thread_data['username_author_color'] = '#fff'
|
611
|
+
if username_author_tag:
|
612
|
+
for style, color in ROLE_COLOR.items():
|
613
|
+
if style in str(username_author_tag):
|
614
|
+
thread_data['username_author_color'] = color
|
615
|
+
break
|
616
|
+
|
617
|
+
start_date_li = minor_div.find('li', 'structItem-startDate') if minor_div else None
|
618
|
+
time_tag = start_date_li.find('time', class_='u-dt') if start_date_li else None
|
619
|
+
created_date = time_tag.get('data-time') if time_tag else None
|
620
|
+
thread_data['created_date'] = int(created_date) if created_date and created_date.isdigit() else None
|
621
|
+
|
622
|
+
latest_cell = thread.find('div', 'structItem-cell--latest')
|
623
|
+
last_message_username_tag = latest_cell.find('div', 'structItem-minor').find(class_=compile('username')) if latest_cell else None
|
624
|
+
thread_data['username_last_message'] = last_message_username_tag.text.strip() if last_message_username_tag else None
|
625
|
+
|
626
|
+
thread_data['username_last_message_color'] = '#fff'
|
627
|
+
if last_message_username_tag:
|
628
|
+
for style, color in ROLE_COLOR.items():
|
629
|
+
if style in str(last_message_username_tag):
|
630
|
+
thread_data['username_last_message_color'] = color
|
631
|
+
break
|
632
|
+
|
633
|
+
latest_date_tag = latest_cell.find('time', class_='structItem-latestDate') if latest_cell else None
|
634
|
+
last_message_date = latest_date_tag.get('data-time') if latest_date_tag else None
|
635
|
+
thread_data['last_message_date'] = int(last_message_date) if last_message_date and last_message_date.isdigit() else None
|
636
|
+
|
637
|
+
thread_data['thread_id'] = thread_id
|
638
|
+
thread_data['is_pinned'] = len(thread.find_all('i', {'title': 'Закреплено'})) > 0
|
639
|
+
thread_data['is_closed'] = len(thread.find_all('i', {'title': 'Закрыта'})) > 0
|
640
|
+
|
641
|
+
result.append(thread_data)
|
642
|
+
|
643
|
+
return result
|
644
|
+
except aiohttp.ClientError as e:
|
645
|
+
print(f"Ошибка сети при получении расширенных тем из категории {category_id} (страница {page}): {e}")
|
646
|
+
return None
|
647
|
+
except Exception as e:
|
648
|
+
print(f"Неожиданная ошибка при получении расширенных тем из категории {category_id} (страница {page}): {e}")
|
649
|
+
return None
|
650
|
+
|
651
|
+
|
652
|
+
async def get_parent_category_of_category(self, category_id: int) -> Optional[Category]:
|
653
|
+
if not self._session or self._session.closed:
|
654
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
655
|
+
url = f"{MAIN_URL}/forums/{category_id}"
|
656
|
+
try:
|
657
|
+
async with self._session.get(url) as response:
|
658
|
+
response.raise_for_status()
|
659
|
+
html_content = await response.text()
|
660
|
+
soup = BeautifulSoup(html_content, 'lxml')
|
661
|
+
|
662
|
+
breadcrumbs = soup.find('ul', {'class': 'p-breadcrumbs'})
|
663
|
+
if not breadcrumbs: return None
|
664
|
+
parent_li = breadcrumbs.find_all('li')
|
665
|
+
if len(parent_li) < 2: return None
|
666
|
+
parent_link = parent_li[-1].find('a')
|
667
|
+
if not parent_link or not parent_link.get('href'): return None
|
668
|
+
|
669
|
+
href_parts = parent_link['href'].split('/')
|
670
|
+
if len(href_parts) < 3: return None
|
671
|
+
parent_category_id_str = href_parts[2]
|
672
|
+
|
673
|
+
if not parent_category_id_str.isdigit():
|
674
|
+
return None
|
675
|
+
|
676
|
+
parent_category_id = int(parent_category_id_str)
|
677
|
+
return await self.get_category(parent_category_id)
|
678
|
+
except aiohttp.ClientError as e:
|
679
|
+
print(f"Ошибка сети при получении родительской категории для {category_id}: {e}")
|
680
|
+
return None
|
681
|
+
except Exception as e:
|
682
|
+
print(f"Неожиданная ошибка при получении родительской категории для {category_id}: {e}")
|
683
|
+
return None
|
684
|
+
|
685
|
+
|
686
|
+
async def get_categories(self, category_id: int) -> Optional[List[int]]:
|
687
|
+
if not self._session or self._session.closed:
|
688
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
689
|
+
token = await self.token
|
690
|
+
url = f"{MAIN_URL}/forums/{category_id}/page-1" # page-1 может быть багом в оригинале?
|
691
|
+
params = {'_xfResponseType': 'json', '_xfToken': token}
|
692
|
+
try:
|
693
|
+
async with self._session.get(url, params=params) as response:
|
694
|
+
response.raise_for_status()
|
695
|
+
data = await response.json()
|
696
|
+
|
697
|
+
if data.get('status') == 'error':
|
698
|
+
return None
|
699
|
+
|
700
|
+
html_content = unescape(data['html']['content'])
|
701
|
+
soup = BeautifulSoup(html_content, "lxml")
|
702
|
+
categories = []
|
703
|
+
for category_div in soup.find_all('div', compile('.*node--depth2 node--forum.*')):
|
704
|
+
link = category_div.find("a")
|
705
|
+
if link and link.get('href'):
|
706
|
+
ids = findall(r'\d+', link['href'])
|
707
|
+
if ids:
|
708
|
+
categories.append(int(ids[0]))
|
709
|
+
return categories
|
710
|
+
except aiohttp.ClientError as e:
|
711
|
+
print(f"Ошибка сети при получении дочерних категорий из {category_id}: {e}")
|
712
|
+
return None
|
713
|
+
except Exception as e:
|
714
|
+
print(f"Неожиданная ошибка при получении дочерних категорий из {category_id}: {e}")
|
715
|
+
return None
|
716
|
+
|
717
|
+
|
718
|
+
# MEMBER
|
719
|
+
async def follow_member(self, member_id: int) -> aiohttp.ClientResponse:
|
720
|
+
if member_id == self.current_member.id:
|
721
|
+
raise ThisIsYouError(member_id)
|
722
|
+
if not self._session or self._session.closed:
|
723
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
724
|
+
token = await self.token
|
725
|
+
url = f"{MAIN_URL}/members/{member_id}/follow"
|
726
|
+
payload = {'_xfToken': token}
|
727
|
+
try:
|
728
|
+
response = await self._session.post(url, data=payload)
|
729
|
+
return response
|
730
|
+
except aiohttp.ClientError as e:
|
731
|
+
print(f"Ошибка сети при подписке/отписке от пользователя {member_id}: {e}")
|
732
|
+
raise e
|
733
|
+
|
734
|
+
|
735
|
+
async def ignore_member(self, member_id: int) -> aiohttp.ClientResponse:
|
736
|
+
if member_id == self.current_member.id:
|
737
|
+
raise ThisIsYouError(member_id)
|
738
|
+
if not self._session or self._session.closed:
|
739
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
740
|
+
token = await self.token
|
741
|
+
url = f"{MAIN_URL}/members/{member_id}/ignore"
|
742
|
+
payload = {'_xfToken': token}
|
743
|
+
try:
|
744
|
+
response = await self._session.post(url, data=payload)
|
745
|
+
return response
|
746
|
+
except aiohttp.ClientError as e:
|
747
|
+
print(f"Ошибка сети при игнорировании/отмене игнорирования пользователя {member_id}: {e}")
|
748
|
+
raise e
|
749
|
+
|
750
|
+
|
751
|
+
async def add_profile_message(self, member_id: int, message_html: str) -> aiohttp.ClientResponse:
|
752
|
+
if not self._session or self._session.closed:
|
753
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
754
|
+
token = await self.token
|
755
|
+
url = f"{MAIN_URL}/members/{member_id}/post"
|
756
|
+
payload = {'_xfToken': token, 'message_html': message_html}
|
757
|
+
try:
|
758
|
+
response = await self._session.post(url, data=payload)
|
759
|
+
return response
|
760
|
+
except aiohttp.ClientError as e:
|
761
|
+
print(f"Ошибка сети при добавлении сообщения на стену пользователя {member_id}: {e}")
|
762
|
+
raise e
|
763
|
+
|
764
|
+
|
765
|
+
async def get_profile_messages(self, member_id: int, page: int = 1) -> Optional[List[int]]:
|
766
|
+
if not self._session or self._session.closed:
|
767
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
768
|
+
token = await self.token
|
769
|
+
url = f"{MAIN_URL}/members/{member_id}/page-{page}"
|
770
|
+
params = {'_xfResponseType': 'json', '_xfToken': token}
|
771
|
+
try:
|
772
|
+
async with self._session.get(url, params=params) as response:
|
773
|
+
response.raise_for_status()
|
774
|
+
data = await response.json()
|
775
|
+
|
776
|
+
if data.get('status') == 'error':
|
777
|
+
return None
|
778
|
+
|
779
|
+
html_content = unescape(data['html']['content'])
|
780
|
+
soup = BeautifulSoup(html_content, "lxml")
|
781
|
+
messages = []
|
782
|
+
for post in soup.find_all('article', {'id': compile('js-profilePost-*')}):
|
783
|
+
post_id_str = post.get('id', '').strip('js-profilePost-')
|
784
|
+
if post_id_str.isdigit():
|
785
|
+
messages.append(int(post_id_str))
|
786
|
+
return messages
|
787
|
+
except aiohttp.ClientError as e:
|
788
|
+
print(f"Ошибка сети при получении сообщений профиля {member_id} (страница {page}): {e}")
|
789
|
+
return None
|
790
|
+
except Exception as e:
|
791
|
+
print(f"Неожиданная ошибка при получении сообщений профиля {member_id} (страница {page}): {e}")
|
792
|
+
return None
|
793
|
+
|
794
|
+
|
795
|
+
# POST
|
796
|
+
async def react_post(self, post_id: int, reaction_id: int = 1) -> aiohttp.ClientResponse:
|
797
|
+
if not self._session or self._session.closed:
|
798
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
799
|
+
token = await self.token
|
800
|
+
url = f'{MAIN_URL}/posts/{post_id}/react'
|
801
|
+
params = {'reaction_id': str(reaction_id)}
|
802
|
+
payload = {'_xfToken': token}
|
803
|
+
try:
|
804
|
+
response = await self._session.post(url, params=params, data=payload)
|
805
|
+
return response
|
806
|
+
except aiohttp.ClientError as e:
|
807
|
+
print(f"Ошибка сети при установке реакции на пост {post_id}: {e}")
|
808
|
+
raise e
|
809
|
+
|
810
|
+
|
811
|
+
async def edit_post(self, post_id: int, message_html: str) -> aiohttp.ClientResponse:
|
812
|
+
if not self._session or self._session.closed:
|
813
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
814
|
+
token = await self.token
|
815
|
+
|
816
|
+
post_info = await self.get_post(post_id)
|
817
|
+
if not post_info or not post_info.thread:
|
818
|
+
raise ValueError(f"Не удалось получить информацию о посте {post_id} для редактирования")
|
819
|
+
else:
|
820
|
+
title_of_thread_post = post_info.thread.title
|
821
|
+
|
822
|
+
url = f"{MAIN_URL}/posts/{post_id}/edit"
|
823
|
+
payload = {
|
824
|
+
"_xfToken": token,
|
825
|
+
"title": title_of_thread_post,
|
826
|
+
"message_html": message_html,
|
827
|
+
"message": message_html
|
828
|
+
}
|
829
|
+
try:
|
830
|
+
response = await self._session.post(url, data=payload)
|
831
|
+
return response
|
832
|
+
except aiohttp.ClientError as e:
|
833
|
+
print(f"Ошибка сети при редактировании поста {post_id}: {e}")
|
834
|
+
raise e
|
835
|
+
|
836
|
+
|
837
|
+
async def delete_post(self, post_id: int, reason: str, hard_delete: bool = False) -> aiohttp.ClientResponse:
|
838
|
+
if not self._session or self._session.closed:
|
839
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
840
|
+
token = await self.token
|
841
|
+
url = f"{MAIN_URL}/posts/{post_id}/delete"
|
842
|
+
payload = {
|
843
|
+
"_xfToken": token,
|
844
|
+
"reason": reason,
|
845
|
+
"hard_delete": int(hard_delete)
|
846
|
+
}
|
847
|
+
try:
|
848
|
+
response = await self._session.post(url, data=payload)
|
849
|
+
return response
|
850
|
+
except aiohttp.ClientError as e:
|
851
|
+
print(f"Ошибка сети при удалении поста {post_id}: {e}")
|
852
|
+
raise e
|
853
|
+
|
854
|
+
async def bookmark_post(self, post_id: int) -> aiohttp.ClientResponse:
|
855
|
+
if not self._session or self._session.closed:
|
856
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
857
|
+
token = await self.token
|
858
|
+
url = f"{MAIN_URL}/posts/{post_id}/bookmark"
|
859
|
+
payload = {"_xfToken": token}
|
860
|
+
try:
|
861
|
+
response = await self._session.post(url, data=payload)
|
862
|
+
return response
|
863
|
+
except aiohttp.ClientError as e:
|
864
|
+
print(f"Ошибка сети при добавлении поста {post_id} в закладки: {e}")
|
865
|
+
raise e
|
866
|
+
|
867
|
+
# PROFILE POST
|
868
|
+
async def react_profile_post(self, post_id: int, reaction_id: int = 1) -> aiohttp.ClientResponse:
|
869
|
+
if not self._session or self._session.closed:
|
870
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
871
|
+
token = await self.token
|
872
|
+
url = f'{MAIN_URL}/profile-posts/{post_id}/react'
|
873
|
+
params = {'reaction_id': str(reaction_id)}
|
874
|
+
payload = {'_xfToken': token}
|
875
|
+
try:
|
876
|
+
response = await self._session.post(url, params=params, data=payload)
|
877
|
+
return response
|
878
|
+
except aiohttp.ClientError as e:
|
879
|
+
print(f"Ошибка сети при установке реакции на пост профиля {post_id}: {e}")
|
880
|
+
raise e
|
881
|
+
|
882
|
+
|
883
|
+
async def comment_profile_post(self, post_id: int, message_html: str) -> aiohttp.ClientResponse:
|
884
|
+
if not self._session or self._session.closed:
|
885
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
886
|
+
token = await self.token
|
887
|
+
url = f"{MAIN_URL}/profile-posts/{post_id}/add-comment"
|
888
|
+
payload = {"_xfToken": token, "message_html": message_html}
|
889
|
+
try:
|
890
|
+
response = await self._session.post(url, data=payload)
|
891
|
+
return response
|
892
|
+
except aiohttp.ClientError as e:
|
893
|
+
print(f"Ошибка сети при комментировании поста профиля {post_id}: {e}")
|
894
|
+
raise e
|
895
|
+
|
896
|
+
|
897
|
+
async def delete_profile_post(self, post_id: int, reason: str, hard_delete: bool = False) -> aiohttp.ClientResponse:
|
898
|
+
if not self._session or self._session.closed:
|
899
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
900
|
+
token = await self.token
|
901
|
+
url = f"{MAIN_URL}/profile-posts/{post_id}/delete"
|
902
|
+
payload = {
|
903
|
+
"_xfToken": token,
|
904
|
+
"reason": reason,
|
905
|
+
"hard_delete": int(hard_delete)
|
906
|
+
}
|
907
|
+
try:
|
908
|
+
response = await self._session.post(url, data=payload)
|
909
|
+
return response
|
910
|
+
except aiohttp.ClientError as e:
|
911
|
+
print(f"Ошибка сети при удалении поста профиля {post_id}: {e}")
|
912
|
+
raise e
|
913
|
+
|
914
|
+
|
915
|
+
async def edit_profile_post(self, post_id: int, message_html: str) -> aiohttp.ClientResponse:
|
916
|
+
if not self._session or self._session.closed:
|
917
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
918
|
+
token = await self.token
|
919
|
+
url = f"{MAIN_URL}/profile-posts/{post_id}/edit"
|
920
|
+
payload = {
|
921
|
+
"_xfToken": token,
|
922
|
+
"message_html": message_html,
|
923
|
+
"message": message_html
|
924
|
+
}
|
925
|
+
try:
|
926
|
+
response = await self._session.post(url, data=payload)
|
927
|
+
return response
|
928
|
+
except aiohttp.ClientError as e:
|
929
|
+
print(f"Ошибка сети при редактировании поста профиля {post_id}: {e}")
|
930
|
+
raise e
|
931
|
+
|
932
|
+
async def answer_thread(self, thread_id: int, message_html: str) -> aiohttp.ClientResponse:
|
933
|
+
if not self._session or self._session.closed:
|
934
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
935
|
+
token = await self.token
|
936
|
+
url = f"{MAIN_URL}/threads/{thread_id}/add-reply"
|
937
|
+
payload = {
|
938
|
+
'_xfToken': token,
|
939
|
+
'message_html': message_html
|
940
|
+
}
|
941
|
+
try:
|
942
|
+
async with self._session.post(url, data=payload) as response:
|
943
|
+
return response
|
944
|
+
except aiohttp.ClientError as e:
|
945
|
+
print(f"Ошибка сети при ответе в теме {thread_id}: {e}")
|
946
|
+
raise e
|
947
|
+
|
948
|
+
async def watch_thread(self, thread_id: int, email_subscribe: bool = False, stop: bool = False) -> aiohttp.ClientResponse:
|
949
|
+
if not self._session or self._session.closed:
|
950
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
951
|
+
token = await self.token
|
952
|
+
url = f"{MAIN_URL}/threads/{thread_id}/watch"
|
953
|
+
payload = {
|
954
|
+
'_xfToken': token,
|
955
|
+
'stop': int(stop),
|
956
|
+
'email_subscribe': int(email_subscribe)
|
957
|
+
}
|
958
|
+
try:
|
959
|
+
async with self._session.post(url, data=payload) as response:
|
960
|
+
return response
|
961
|
+
except aiohttp.ClientError as e:
|
962
|
+
print(f"Ошибка сети при изменении статуса отслеживания темы {thread_id}: {e}")
|
963
|
+
raise e
|
964
|
+
|
965
|
+
async def delete_thread(self, thread_id: int, reason: str, hard_delete: bool = False) -> aiohttp.ClientResponse:
|
966
|
+
if not self._session or self._session.closed:
|
967
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
968
|
+
token = await self.token
|
969
|
+
url = f"{MAIN_URL}/threads/{thread_id}/delete"
|
970
|
+
payload = {
|
971
|
+
"reason": reason,
|
972
|
+
"hard_delete": int(hard_delete),
|
973
|
+
"_xfToken": token
|
974
|
+
}
|
975
|
+
try:
|
976
|
+
async with self._session.post(url, data=payload) as response:
|
977
|
+
return response
|
978
|
+
except aiohttp.ClientError as e:
|
979
|
+
print(f"Ошибка сети при удалении темы {thread_id}: {e}")
|
980
|
+
raise e
|
981
|
+
|
982
|
+
async def edit_thread(self, thread_id: int, message_html: str) -> Optional[aiohttp.ClientResponse]:
|
983
|
+
if not self._session or self._session.closed:
|
984
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
985
|
+
token = await self.token
|
986
|
+
get_url = f"{MAIN_URL}/threads/{thread_id}/page-1"
|
987
|
+
thread_post_id = None
|
988
|
+
|
989
|
+
try:
|
990
|
+
async with self._session.get(get_url) as response:
|
991
|
+
response.raise_for_status()
|
992
|
+
html_content = await response.text()
|
993
|
+
soup = BeautifulSoup(html_content, 'lxml')
|
994
|
+
post_article = soup.find('article', {'id': compile('js-post-*')})
|
995
|
+
if post_article and 'id' in post_article.attrs:
|
996
|
+
thread_post_id = post_article['id'].strip('js-post-')
|
997
|
+
else:
|
998
|
+
print(f"Не удалось найти ID первого поста для темы {thread_id}")
|
999
|
+
return None
|
1000
|
+
|
1001
|
+
except aiohttp.ClientError as e:
|
1002
|
+
print(f"Ошибка сети при получении информации для редактирования темы {thread_id}: {e}")
|
1003
|
+
return None
|
1004
|
+
except Exception as e:
|
1005
|
+
print(f"Ошибка парсинга при получении информации для редактирования темы {thread_id}: {e}")
|
1006
|
+
return None
|
1007
|
+
|
1008
|
+
if not thread_post_id:
|
1009
|
+
return None
|
1010
|
+
|
1011
|
+
edit_url = f"{MAIN_URL}/posts/{thread_post_id}/edit"
|
1012
|
+
payload = {
|
1013
|
+
"message_html": message_html,
|
1014
|
+
"message": message_html,
|
1015
|
+
"_xfToken": token
|
1016
|
+
}
|
1017
|
+
try:
|
1018
|
+
async with self._session.post(edit_url, data=payload) as response:
|
1019
|
+
return response
|
1020
|
+
except aiohttp.ClientError as e:
|
1021
|
+
print(f"Ошибка сети при редактировании темы {thread_id} (пост {thread_post_id}): {e}")
|
1022
|
+
raise e
|
1023
|
+
|
1024
|
+
|
1025
|
+
async def edit_thread_info(self, thread_id: int, title: str, prefix_id: Optional[int] = None, sticky: bool = True, opened: bool = True) -> aiohttp.ClientResponse:
|
1026
|
+
if not self._session or self._session.closed:
|
1027
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1028
|
+
token = await self.token
|
1029
|
+
url = f"{MAIN_URL}/threads/{thread_id}/edit"
|
1030
|
+
payload = {
|
1031
|
+
"_xfToken": token,
|
1032
|
+
'title': title
|
1033
|
+
}
|
1034
|
+
|
1035
|
+
if prefix_id is not None:
|
1036
|
+
payload['prefix_id'] = prefix_id
|
1037
|
+
if opened:
|
1038
|
+
payload["discussion_open"] = 1
|
1039
|
+
else:
|
1040
|
+
pass
|
1041
|
+
if sticky:
|
1042
|
+
payload["sticky"] = 1
|
1043
|
+
|
1044
|
+
try:
|
1045
|
+
async with self._session.post(url, data=payload) as response:
|
1046
|
+
return response
|
1047
|
+
except aiohttp.ClientError as e:
|
1048
|
+
print(f"Ошибка сети при изменении информации темы {thread_id}: {e}")
|
1049
|
+
raise e
|
1050
|
+
|
1051
|
+
|
1052
|
+
async def get_thread_category(self, thread_id: int) -> Optional['Category']:
|
1053
|
+
if not self._session or self._session.closed:
|
1054
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1055
|
+
|
1056
|
+
url = f"{MAIN_URL}/threads/{thread_id}/page-1"
|
1057
|
+
try:
|
1058
|
+
async with self._session.get(url) as response:
|
1059
|
+
response.raise_for_status()
|
1060
|
+
html_content = await response.text()
|
1061
|
+
soup = BeautifulSoup(html_content, 'lxml')
|
1062
|
+
|
1063
|
+
html_tag = soup.find('html')
|
1064
|
+
if not html_tag or 'data-container-key' not in html_tag.attrs:
|
1065
|
+
print(f"Не удалось найти data-container-key для темы {thread_id}")
|
1066
|
+
return None
|
1067
|
+
|
1068
|
+
container_key = html_tag['data-container-key']
|
1069
|
+
if not container_key.startswith('node-'):
|
1070
|
+
print(f"Некорректный data-container-key '{container_key}' для темы {thread_id}")
|
1071
|
+
return None
|
1072
|
+
|
1073
|
+
category_id_str = container_key.strip('node-')
|
1074
|
+
try:
|
1075
|
+
category_id = int(category_id_str)
|
1076
|
+
except ValueError:
|
1077
|
+
print(f"Не удалось преобразовать ID категории '{category_id_str}' в число для темы {thread_id}")
|
1078
|
+
return None
|
1079
|
+
|
1080
|
+
return await self.get_category(category_id)
|
1081
|
+
|
1082
|
+
except aiohttp.ClientError as e:
|
1083
|
+
print(f"Ошибка сети при получении категории темы {thread_id}: {e}")
|
1084
|
+
return None
|
1085
|
+
except Exception as e:
|
1086
|
+
print(f"Ошибка парсинга при получении категории темы {thread_id}: {e}")
|
1087
|
+
return None
|
1088
|
+
|
1089
|
+
async def get_thread_posts(self, thread_id: int, page: int = 1) -> Optional[List[str]]:
|
1090
|
+
if not self._session or self._session.closed:
|
1091
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1092
|
+
token = await self.token
|
1093
|
+
url = f"{MAIN_URL}/threads/{thread_id}/page-{page}"
|
1094
|
+
params = {'_xfResponseType': 'json', '_xfToken': token}
|
1095
|
+
try:
|
1096
|
+
async with self._session.get(url, params=params) as response:
|
1097
|
+
if response.status == 404:
|
1098
|
+
return []
|
1099
|
+
response.raise_for_status()
|
1100
|
+
data = await response.json()
|
1101
|
+
|
1102
|
+
if data.get('status') == 'error':
|
1103
|
+
return None
|
1104
|
+
|
1105
|
+
if 'html' not in data or 'content' not in data['html']:
|
1106
|
+
return []
|
1107
|
+
|
1108
|
+
soup = BeautifulSoup(unescape(data['html']['content']), "lxml")
|
1109
|
+
posts = soup.find_all('article', {'id': compile('js-post-*')})
|
1110
|
+
return [i['id'].strip('js-post-') for i in posts if 'id' in i.attrs]
|
1111
|
+
|
1112
|
+
except aiohttp.ClientError as e:
|
1113
|
+
print(f"Ошибка сети при получении постов темы {thread_id}, стр {page}: {e}")
|
1114
|
+
return None
|
1115
|
+
except Exception as e:
|
1116
|
+
print(f"Неожиданная ошибка при получении постов темы {thread_id}, стр {page}: {e}")
|
1117
|
+
return None
|
1118
|
+
|
1119
|
+
async def get_all_thread_posts(self, thread_id: int) -> List[str]:
|
1120
|
+
if not self._session or self._session.closed:
|
1121
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1122
|
+
token = await self.token
|
1123
|
+
|
1124
|
+
all_posts_ids = []
|
1125
|
+
page = 1
|
1126
|
+
pages_count = 1
|
1127
|
+
processed_first_page = False
|
1128
|
+
|
1129
|
+
while True:
|
1130
|
+
url = f"{MAIN_URL}/threads/{thread_id}/page-{page}"
|
1131
|
+
params = {'_xfResponseType': 'json', '_xfToken': token}
|
1132
|
+
try:
|
1133
|
+
async with self._session.get(url, params=params) as response:
|
1134
|
+
if response.status == 404 and page > 1:
|
1135
|
+
break
|
1136
|
+
response.raise_for_status()
|
1137
|
+
data = await response.json()
|
1138
|
+
|
1139
|
+
if data.get('status') == 'error':
|
1140
|
+
if page == 1:
|
1141
|
+
print(f"API вернуло ошибку на первой странице темы {thread_id}: {data.get('errors')}")
|
1142
|
+
break
|
1143
|
+
|
1144
|
+
if 'html' not in data or 'content' not in data['html']:
|
1145
|
+
print(f"Ответ API для темы {thread_id} стр {page} не содержит HTML.")
|
1146
|
+
if page == 1:
|
1147
|
+
return []
|
1148
|
+
else:
|
1149
|
+
break
|
1150
|
+
|
1151
|
+
html_content = unescape(data['html']['content'])
|
1152
|
+
soup = BeautifulSoup(html_content, "lxml")
|
1153
|
+
current_page_posts = soup.find_all('article', {'id': compile('js-post-*')})
|
1154
|
+
post_ids = [i['id'].strip('js-post-') for i in current_page_posts if 'id' in i.attrs]
|
1155
|
+
|
1156
|
+
if not post_ids and page > 1:
|
1157
|
+
break
|
1158
|
+
|
1159
|
+
all_posts_ids.extend(post_ids)
|
1160
|
+
|
1161
|
+
if not processed_first_page:
|
1162
|
+
pages_count = 1
|
1163
|
+
try:
|
1164
|
+
page_nav = soup.find('ul', class_='pageNav-main')
|
1165
|
+
if page_nav:
|
1166
|
+
last_page_li = page_nav.find_all('li', class_='pageNav-page')
|
1167
|
+
if last_page_li:
|
1168
|
+
pages_count = int(last_page_li[-1].text)
|
1169
|
+
except (IndexError, AttributeError, ValueError, TypeError):
|
1170
|
+
pages_count = 1
|
1171
|
+
processed_first_page = True
|
1172
|
+
|
1173
|
+
if page >= pages_count:
|
1174
|
+
break
|
1175
|
+
|
1176
|
+
page += 1
|
1177
|
+
|
1178
|
+
except aiohttp.ClientError as e:
|
1179
|
+
print(f"Ошибка сети при получении всех постов темы {thread_id}, стр {page}: {e}")
|
1180
|
+
break
|
1181
|
+
except Exception as e:
|
1182
|
+
print(f"Неожиданная ошибка при получении всех постов темы {thread_id}, стр {page}: {e}")
|
1183
|
+
break
|
1184
|
+
|
1185
|
+
return all_posts_ids
|
1186
|
+
|
1187
|
+
|
1188
|
+
async def react_thread(self, thread_id: int, reaction_id: int = 1) -> Optional[aiohttp.ClientResponse]:
|
1189
|
+
if not self._session or self._session.closed:
|
1190
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1191
|
+
token = await self.token
|
1192
|
+
get_url = f"{MAIN_URL}/threads/{thread_id}/page-1"
|
1193
|
+
thread_post_id = None
|
1194
|
+
|
1195
|
+
try:
|
1196
|
+
async with self._session.get(get_url) as response:
|
1197
|
+
response.raise_for_status()
|
1198
|
+
html_content = await response.text()
|
1199
|
+
soup = BeautifulSoup(html_content, 'lxml')
|
1200
|
+
post_article = soup.find('article', {'id': compile('js-post-*')})
|
1201
|
+
if post_article and 'id' in post_article.attrs:
|
1202
|
+
thread_post_id = post_article['id'].strip('js-post-')
|
1203
|
+
else:
|
1204
|
+
print(f"Не удалось найти ID первого поста для реакции в теме {thread_id}")
|
1205
|
+
return None
|
1206
|
+
|
1207
|
+
except aiohttp.ClientError as e:
|
1208
|
+
print(f"Ошибка сети при получении ID поста для реакции в теме {thread_id}: {e}")
|
1209
|
+
return None
|
1210
|
+
except Exception as e:
|
1211
|
+
print(f"Ошибка парсинга при получении ID поста для реакции в теме {thread_id}: {e}")
|
1212
|
+
return None
|
1213
|
+
|
1214
|
+
if not thread_post_id:
|
1215
|
+
return None
|
1216
|
+
|
1217
|
+
react_url = f'{MAIN_URL}/posts/{thread_post_id}/react'
|
1218
|
+
params = {'reaction_id': str(reaction_id)}
|
1219
|
+
payload = {'_xfToken': token}
|
1220
|
+
|
1221
|
+
try:
|
1222
|
+
async with self._session.post(react_url, params=params, data=payload) as response:
|
1223
|
+
return response
|
1224
|
+
except aiohttp.ClientError as e:
|
1225
|
+
print(f"Ошибка сети при установке реакции {reaction_id} на пост {thread_post_id} темы {thread_id}: {e}")
|
1226
|
+
raise e
|
1227
|
+
|
1228
|
+
# OTHER
|
1229
|
+
async def send_form(self, form_id: int, data: dict) -> aiohttp.ClientResponse:
|
1230
|
+
"""Заполнить форму
|
1231
|
+
|
1232
|
+
Attributes:
|
1233
|
+
form_id (int): ID формы
|
1234
|
+
data (dict): Информация для запонения в виде словаря. Форма словаря: {'question[id вопроса]' = 'необходимая информация'} | Пример: {'question[531]' = '1'}
|
1235
|
+
|
1236
|
+
Returns:
|
1237
|
+
Объект Response модуля aiohttp
|
1238
|
+
"""
|
1239
|
+
if not self._session or self._session.closed:
|
1240
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1241
|
+
|
1242
|
+
data.update({'_xfToken': await self.token})
|
1243
|
+
try:
|
1244
|
+
async with self._session.post(f"{MAIN_URL}/form/{form_id}/submit", data=data) as response:
|
1245
|
+
return response
|
1246
|
+
except aiohttp.ClientError as e:
|
1247
|
+
print(f"Ошибка сети при отправке формы {form_id}: {e}")
|
1248
|
+
raise e
|
1249
|
+
|
1250
|
+
async def get_notifications(self) -> list:
|
1251
|
+
if not self._session or self._session.closed:
|
1252
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1253
|
+
url = f"{MAIN_URL}/account/alerts"
|
1254
|
+
notifications = []
|
1255
|
+
try:
|
1256
|
+
async with self._session.get(url) as response:
|
1257
|
+
response.raise_for_status()
|
1258
|
+
html_content = await response.text()
|
1259
|
+
soup = BeautifulSoup(html_content, 'lxml')
|
1260
|
+
|
1261
|
+
for alert in soup.find_all('li', {'class': 'js-alert'}):
|
1262
|
+
if not alert.has_attr('data-alert-id'):
|
1263
|
+
continue
|
1264
|
+
|
1265
|
+
sender = None
|
1266
|
+
username_link = alert.find('a', {'class': 'username'})
|
1267
|
+
if username_link:
|
1268
|
+
sender_id_str = username_link.get('data-user-id', '0')
|
1269
|
+
sender = {
|
1270
|
+
'id': int(sender_id_str) if sender_id_str.isdigit() else 0,
|
1271
|
+
'name': unescape(username_link.get_text(strip=True)),
|
1272
|
+
'avatar': None,
|
1273
|
+
'avatar_color': None,
|
1274
|
+
'initials': None
|
1275
|
+
}
|
1276
|
+
|
1277
|
+
avatar_container = alert.find('div', class_='contentRow-figure')
|
1278
|
+
if avatar_container:
|
1279
|
+
avatar_img = avatar_container.find('img', {'class': 'avatar'})
|
1280
|
+
avatar_span = avatar_container.find('span', {'class': 'avatar'})
|
1281
|
+
|
1282
|
+
if avatar_img and avatar_img.has_attr('src'):
|
1283
|
+
sender['avatar'] = avatar_img['src']
|
1284
|
+
elif avatar_span and 'avatar--default' in avatar_span.get('class', []):
|
1285
|
+
sender['avatar_color'] = avatar_span.get('style')
|
1286
|
+
sender['initials'] = unescape(avatar_span.get_text(strip=True)) if avatar_span else None
|
1287
|
+
|
1288
|
+
|
1289
|
+
time_tag = alert.find('time', {'class': 'u-dt'})
|
1290
|
+
timestamp = None
|
1291
|
+
if time_tag:
|
1292
|
+
timestamp = {
|
1293
|
+
'iso': time_tag.get('datetime'),
|
1294
|
+
'unix': int(time_tag['data-time']) if time_tag and time_tag.has_attr('data-time') and time_tag['data-time'].isdigit() else None
|
1295
|
+
}
|
1296
|
+
|
1297
|
+
alert_text_container = alert.find('div', {'class': 'contentRow-main'})
|
1298
|
+
alert_text = unescape(alert_text_container.get_text(strip=True)) if alert_text_container else None
|
1299
|
+
|
1300
|
+
link_tag = alert.find('a', {'class': 'fauxBlockLink-blockLink'})
|
1301
|
+
link = link_tag['href'] if link_tag and link_tag.has_attr('href') else None
|
1302
|
+
|
1303
|
+
alert_data = {
|
1304
|
+
'id': alert.get('data-alert-id'),
|
1305
|
+
'is_unread': 'is-unread' in alert.get('class', []),
|
1306
|
+
'text': alert_text,
|
1307
|
+
'link': f"{MAIN_URL}{link}" if link and link.startswith('/') else link,
|
1308
|
+
'sender': sender,
|
1309
|
+
'timestamp': timestamp
|
1310
|
+
}
|
1311
|
+
|
1312
|
+
notifications.append(alert_data)
|
1313
|
+
|
1314
|
+
return notifications
|
1315
|
+
except aiohttp.ClientError as e:
|
1316
|
+
print(f"Ошибка сети при получении уведомлений: {e}")
|
1317
|
+
return []
|
1318
|
+
except Exception as e:
|
1319
|
+
print(f"Неожиданная ошибка при получении уведомлений: {e}")
|
1320
|
+
return []
|
1321
|
+
|
1322
|
+
async def search_threads(self, query: str, sort: str = 'relevance') -> list:
|
1323
|
+
"""Поиск тем по форуму с заданными параметрами
|
1324
|
+
|
1325
|
+
Attributes:
|
1326
|
+
query (str): Поисковый запрос
|
1327
|
+
sort (str): Тип сортировки (relevance, date, etc)
|
1328
|
+
|
1329
|
+
Returns:
|
1330
|
+
Список словарей с информацией о найденных темах
|
1331
|
+
"""
|
1332
|
+
if not self._session or self._session.closed:
|
1333
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1334
|
+
|
1335
|
+
url = f"{MAIN_URL}/search/24587779/?q={query}&o={sort}"
|
1336
|
+
results = []
|
1337
|
+
|
1338
|
+
try:
|
1339
|
+
async with self._session.get(url) as response:
|
1340
|
+
response.raise_for_status()
|
1341
|
+
html_content = await response.text()
|
1342
|
+
content = BeautifulSoup(html_content, 'lxml')
|
1343
|
+
|
1344
|
+
for thread in content.find_all('li', {'class': 'block-row'}):
|
1345
|
+
title_link = thread.find('h3', {'class': 'contentRow-title'}).find('a')
|
1346
|
+
date_tag = thread.find('time', {'class': 'u-dt'})
|
1347
|
+
answers_tag = thread.find(text=re.compile('Ответы: '))
|
1348
|
+
|
1349
|
+
thread_data = {
|
1350
|
+
'title': title_link.text.strip().split('| Причина:')[0].strip(),
|
1351
|
+
'status': thread.find('span', {'class': 'label'}).text if thread.find('span', {'class': 'label'}) else None,
|
1352
|
+
'author': thread['data-author'],
|
1353
|
+
'thread_id': int(title_link['href'].split('/')[-2]),
|
1354
|
+
'create_date': int(date_tag['data-time']) if date_tag else None,
|
1355
|
+
'answers_count': int(answers_tag.split(': ')[1]) if answers_tag else 0,
|
1356
|
+
'forum': thread.find('a', href=re.compile('/forums/')).text if thread.find('a', href=re.compile('/forums/')) else None,
|
1357
|
+
'snippet': thread.find('div', {'class': 'contentRow-snippet'}).text.strip() if thread.find('div', {'class': 'contentRow-snippet'}) else None,
|
1358
|
+
'url': f"{MAIN_URL}{title_link['href']}"
|
1359
|
+
}
|
1360
|
+
|
1361
|
+
results.append(thread_data)
|
1362
|
+
|
1363
|
+
return results
|
1364
|
+
except aiohttp.ClientError as e:
|
1365
|
+
print(f"Ошибка сети при поиске тем по запросу '{query}': {e}")
|
1366
|
+
return []
|
1367
|
+
except Exception as e:
|
1368
|
+
print(f"Неожиданная ошибка при поиске тем '{query}': {e}")
|
1369
|
+
return []
|
1370
|
+
|
1371
|
+
async def mark_notifications_read(self, alert_ids: list[int]) -> aiohttp.ClientResponse:
|
1372
|
+
"""Пометить уведомления как прочитанные"""
|
1373
|
+
if not self._session or self._session.closed:
|
1374
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1375
|
+
|
1376
|
+
data = {
|
1377
|
+
'_xfToken': await self.token,
|
1378
|
+
'alert_id': alert_ids,
|
1379
|
+
'_xfAction': 'toggle',
|
1380
|
+
'_xfWithData': 1
|
1381
|
+
}
|
1382
|
+
|
1383
|
+
try:
|
1384
|
+
async with self._session.post(
|
1385
|
+
f"{MAIN_URL}/account/alert-toggle",
|
1386
|
+
data=data
|
1387
|
+
) as response:
|
1388
|
+
return response
|
1389
|
+
except aiohttp.ClientError as e:
|
1390
|
+
print(f"Ошибка сети при пометке уведомлений {alert_ids} как прочитанных: {e}")
|
1391
|
+
raise e
|
1392
|
+
|
1393
|
+
async def get_post_bbcode(self, thread_id: int, post_id: int) -> str:
|
1394
|
+
"""Получить BB-код поста по его ID.
|
1395
|
+
|
1396
|
+
Сначала получает HTML-содержимое поста,
|
1397
|
+
затем отправляет этот HTML для конвертации в BBCode.
|
1398
|
+
|
1399
|
+
Args:
|
1400
|
+
thread_id: ID темы, содержащей пост.
|
1401
|
+
post_id: ID поста для получения BB-кода.
|
1402
|
+
|
1403
|
+
Returns:
|
1404
|
+
Строка с BB-кодом поста или пустая строка в случае ошибки.
|
1405
|
+
|
1406
|
+
Raises:
|
1407
|
+
Exception: Если сессия не активна.
|
1408
|
+
"""
|
1409
|
+
if not self._session or self._session.closed:
|
1410
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1411
|
+
try:
|
1412
|
+
token = await self.token
|
1413
|
+
except Exception as e:
|
1414
|
+
print(f"Не удалось получить токен: {e}")
|
1415
|
+
return ''
|
1416
|
+
|
1417
|
+
html_content = ''
|
1418
|
+
try:
|
1419
|
+
post = await self.get_post(post_id)
|
1420
|
+
html_content = post.html_content
|
1421
|
+
except aiohttp.ClientError as e:
|
1422
|
+
print(f"Сетевая ошибка при получении HTML для поста {post_id}: {e}")
|
1423
|
+
except Exception as e:
|
1424
|
+
print(f"Неожиданная ошибка при получении HTML для поста {post_id}: {e}")
|
1425
|
+
try:
|
1426
|
+
convert_url = f"{MAIN_URL}/index.php?editor/to-bb-code"
|
1427
|
+
data_post = {
|
1428
|
+
'_xfResponseType': 'json',
|
1429
|
+
'_xfRequestUri': f'/threads/{thread_id}/',
|
1430
|
+
'_xfWithData': 1,
|
1431
|
+
'_xfToken': token,
|
1432
|
+
'html': html_content
|
1433
|
+
}
|
1434
|
+
async with self._session.post(convert_url, data=data_post) as response:
|
1435
|
+
response.raise_for_status()
|
1436
|
+
convert_data = await response.json()
|
1437
|
+
if convert_data.get("status") == "ok" and "bbCode" in convert_data:
|
1438
|
+
bbcode = convert_data.get('bbCode', '')
|
1439
|
+
return unescape(bbcode)
|
1440
|
+
else:
|
1441
|
+
print(f"Ошибка при конвертации BBCode для поста {post_id}. Ответ сервера: {convert_data}")
|
1442
|
+
return ''
|
1443
|
+
|
1444
|
+
except aiohttp.ClientError as e:
|
1445
|
+
print(f"Сетевая ошибка при конвертации BBCode для поста {post_id}: {e}")
|
1446
|
+
return ''
|
1447
|
+
except Exception as e:
|
1448
|
+
print(f"Неожиданная ошибка при конвертации BBCode для поста {post_id}: {e}")
|
1449
|
+
return ''
|