arizona-forum-api-async 1.0__py3-none-any.whl → 1.1__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 → arizona_forum_api_async-1.1.dist-info}/METADATA +2 -11
- {arizona_forum_api_async-1.0.dist-info → arizona_forum_api_async-1.1.dist-info}/RECORD +6 -7
- {arizona_forum_api_async-1.0.dist-info → arizona_forum_api_async-1.1.dist-info}/WHEEL +1 -1
- arizona_forum_async/api.py +408 -11
- arizona_forum_async/bypass_antibot/script.py +0 -3
- arizona_forum_api_async-1.0.dist-info/licenses/LICENSE +0 -21
- {arizona_forum_api_async-1.0.dist-info → arizona_forum_api_async-1.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: arizona-forum-api-async
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.1
|
4
4
|
Summary: Асинхронная Python библиотека для взаимодействия с форумом Arizona RP (forum.arizona-rp.com) без необходимости получения API ключа.
|
5
5
|
Home-page: https://github.com/fakelag28/Arizona-Forum-API-Async
|
6
6
|
Author: fakelag28
|
@@ -10,7 +10,6 @@ Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
11
11
|
Requires-Python: >=3.6
|
12
12
|
Description-Content-Type: text/markdown
|
13
|
-
License-File: LICENSE
|
14
13
|
Requires-Dist: aiohttp
|
15
14
|
Requires-Dist: aiohttp-socks
|
16
15
|
Requires-Dist: beautifulsoup4
|
@@ -22,7 +21,6 @@ Dynamic: classifier
|
|
22
21
|
Dynamic: description
|
23
22
|
Dynamic: description-content-type
|
24
23
|
Dynamic: home-page
|
25
|
-
Dynamic: license-file
|
26
24
|
Dynamic: requires-dist
|
27
25
|
Dynamic: requires-python
|
28
26
|
Dynamic: summary
|
@@ -31,7 +29,6 @@ Dynamic: summary
|
|
31
29
|
|
32
30
|
[](https://pypi.org/project/arizona-forum-api-async/)
|
33
31
|
[](https://pypi.org/project/arizona-forum-api-async/)
|
34
|
-
[](https://opensource.org/licenses/MIT)
|
35
32
|
[](https://pepy.tech/project/arizona-forum-api-async)
|
36
33
|
|
37
34
|
**Асинхронная Python библиотека для взаимодействия с форумом Arizona RP (forum.arizona-rp.com) без необходимости получения API ключа.**
|
@@ -80,12 +77,6 @@ pip install arizona-forum-api-async
|
|
80
77
|
|
81
78
|
## Документация и примеры
|
82
79
|
|
83
|
-
* **[Wiki (Расширенная документация синхронной версии)](https://github.com/fakelag28/Arizona-Forum-API-
|
80
|
+
* **[Wiki (Расширенная документация синхронной версии)](https://github.com/fakelag28/Arizona-Forum-API-Extended/wiki/Основная-документация):** Подробная документация для другой [расширенной синхронной версии](https://github.com/fakelag28/Arizona-Forum-API-Extended/).
|
84
81
|
* **[Документация оригинальной библиотеки](https://tastybread123.github.io/Arizona-API/arz_api.html):** Документация для оригинальной синхронной библиотеки.
|
85
82
|
* **[Папка с примерами](https://github.com/fakelag28/Arizona-Forum-API-Async/tree/main/examples):** Практические примеры, демонстрирующие различные возможности библиотеки.
|
86
|
-
|
87
|
-
---
|
88
|
-
|
89
|
-
## Лицензия
|
90
|
-
|
91
|
-
Этот проект лицензирован под **MIT License**.
|
@@ -1,17 +1,16 @@
|
|
1
|
-
arizona_forum_api_async-1.0.dist-info/licenses/LICENSE,sha256=lvvIkvjnIhkBIYnSGe3_UIt3uPDEWcthURuMP8NBvLs,1085
|
2
1
|
arizona_forum_async/__init__.py,sha256=LJfbSfw1rC8SKOl5AON1KYPnuKA8CamlgEaNRbMybI4,68
|
3
|
-
arizona_forum_async/api.py,sha256=
|
2
|
+
arizona_forum_async/api.py,sha256=DJiTVLAW47xqy6R3wl_h-UxsCwKxf5d0n2Kwj70Rc4g,94559
|
4
3
|
arizona_forum_async/consts.py,sha256=AlYiIL9Z5t_HvZfaPKfQc72yg6cbl_VHvEs3GYIo3JU,527
|
5
4
|
arizona_forum_async/exceptions.py,sha256=mcWTrRgl1m5ljMYK2-WlNXWnnKSzI5K7fd_EYxJ6-eo,563
|
6
5
|
arizona_forum_async/bypass_antibot/__init__.py,sha256=8FaH5DlQ4acb-sjj-CUYmLsm1Y2zrB2zPVWayybokGo,21
|
7
|
-
arizona_forum_async/bypass_antibot/script.py,sha256=
|
6
|
+
arizona_forum_async/bypass_antibot/script.py,sha256=vlIkzhWHJOL5hX8YY5hdcI7q9TReNsZJYSEiecq7H0U,35300
|
8
7
|
arizona_forum_async/models/__init__.py,sha256=Oz9cUlLgRC2Wbqd79bE9ffiHjxRIwE_Zguf4t9dDDyE,112
|
9
8
|
arizona_forum_async/models/category_object.py,sha256=6quTtKSOyROK87MKCmrMyBxiMJv65weB0JAxYg95E10,4832
|
10
9
|
arizona_forum_async/models/member_object.py,sha256=8DpJ-IbQkMnEQZAD9hWGRJT4rXECGjKHLiCijL3ja4Y,4882
|
11
10
|
arizona_forum_async/models/other.py,sha256=hC9Hg749RRSULUpZ_l_cT5VwpsrG1MOFGZ8MyP3nuCc,536
|
12
11
|
arizona_forum_async/models/post_object.py,sha256=eJ3YrO1QbYtpCcErV1jfRfGFuXNmpXYwaIZT0h5sjVM,6097
|
13
12
|
arizona_forum_async/models/thread_object.py,sha256=pCrv1yRY01EYEMEbQES6TUeR5yfJGPE0oaS17nTsmxI,5737
|
14
|
-
arizona_forum_api_async-1.
|
15
|
-
arizona_forum_api_async-1.
|
16
|
-
arizona_forum_api_async-1.
|
17
|
-
arizona_forum_api_async-1.
|
13
|
+
arizona_forum_api_async-1.1.dist-info/METADATA,sha256=t4tV5DSxHwH-3jCjdolGDcP1EDIS94zp36Q_eJHST5Y,5135
|
14
|
+
arizona_forum_api_async-1.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
15
|
+
arizona_forum_api_async-1.1.dist-info/top_level.txt,sha256=a9GRkw-bNV0GUAHWG7S7n5lGQvBuNu6dfXWF5WqEw6A,20
|
16
|
+
arizona_forum_api_async-1.1.dist-info/RECORD,,
|
arizona_forum_async/api.py
CHANGED
@@ -3,7 +3,9 @@ from bs4 import BeautifulSoup
|
|
3
3
|
from re import compile, findall
|
4
4
|
import re
|
5
5
|
from html import unescape
|
6
|
-
from typing import List, Dict, Optional, Union
|
6
|
+
from typing import List, Dict, Optional, Union, Tuple
|
7
|
+
from collections import defaultdict
|
8
|
+
import datetime
|
7
9
|
|
8
10
|
from arizona_forum_async.consts import MAIN_URL, ROLE_COLOR
|
9
11
|
from arizona_forum_async.bypass_antibot import bypass_async
|
@@ -22,7 +24,6 @@ class ArizonaAPI:
|
|
22
24
|
self.cookie_str = "; ".join([f"{k}={v}" for k, v in cookie.items()])
|
23
25
|
self._session: aiohttp.ClientSession = None
|
24
26
|
self._token: str = None
|
25
|
-
|
26
27
|
|
27
28
|
async def connect(self, do_bypass: bool = True):
|
28
29
|
"""Асинхронный метод для создания сессии, получения токена и обхода анти-бота."""
|
@@ -139,7 +140,6 @@ class ArizonaAPI:
|
|
139
140
|
print(f"Неожиданная ошибка при получении категории {category_id}: {e}")
|
140
141
|
return None
|
141
142
|
|
142
|
-
|
143
143
|
async def get_member(self, user_id: int) -> 'Member | None':
|
144
144
|
if not self._session or self._session.closed:
|
145
145
|
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
@@ -465,7 +465,7 @@ class ArizonaAPI:
|
|
465
465
|
return None
|
466
466
|
|
467
467
|
|
468
|
-
|
468
|
+
# ---------------================ МЕТОДЫ ОБЪЕКТОВ ====================--------------------
|
469
469
|
|
470
470
|
# CATEGORY
|
471
471
|
async def create_thread(self, category_id: int, title: str, message_html: str, discussion_type: str = 'discussion', watch_thread: bool = True) -> aiohttp.ClientResponse:
|
@@ -562,7 +562,6 @@ class ArizonaAPI:
|
|
562
562
|
print(f"Неожиданная ошибка при получении тем из категории {category_id} (страница {page}): {e}")
|
563
563
|
return None
|
564
564
|
|
565
|
-
|
566
565
|
async def get_thread_category_detail(self, category_id: int, page: int = 1) -> Optional[List[Dict]]:
|
567
566
|
if not self._session or self._session.closed:
|
568
567
|
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
@@ -648,7 +647,6 @@ class ArizonaAPI:
|
|
648
647
|
print(f"Неожиданная ошибка при получении расширенных тем из категории {category_id} (страница {page}): {e}")
|
649
648
|
return None
|
650
649
|
|
651
|
-
|
652
650
|
async def get_parent_category_of_category(self, category_id: int) -> Optional[Category]:
|
653
651
|
if not self._session or self._session.closed:
|
654
652
|
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
@@ -682,7 +680,6 @@ class ArizonaAPI:
|
|
682
680
|
print(f"Неожиданная ошибка при получении родительской категории для {category_id}: {e}")
|
683
681
|
return None
|
684
682
|
|
685
|
-
|
686
683
|
async def get_categories(self, category_id: int) -> Optional[List[int]]:
|
687
684
|
if not self._session or self._session.closed:
|
688
685
|
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
@@ -714,7 +711,6 @@ class ArizonaAPI:
|
|
714
711
|
print(f"Неожиданная ошибка при получении дочерних категорий из {category_id}: {e}")
|
715
712
|
return None
|
716
713
|
|
717
|
-
|
718
714
|
# MEMBER
|
719
715
|
async def follow_member(self, member_id: int) -> aiohttp.ClientResponse:
|
720
716
|
if member_id == self.current_member.id:
|
@@ -731,7 +727,6 @@ class ArizonaAPI:
|
|
731
727
|
print(f"Ошибка сети при подписке/отписке от пользователя {member_id}: {e}")
|
732
728
|
raise e
|
733
729
|
|
734
|
-
|
735
730
|
async def ignore_member(self, member_id: int) -> aiohttp.ClientResponse:
|
736
731
|
if member_id == self.current_member.id:
|
737
732
|
raise ThisIsYouError(member_id)
|
@@ -1021,7 +1016,6 @@ class ArizonaAPI:
|
|
1021
1016
|
print(f"Ошибка сети при редактировании темы {thread_id} (пост {thread_post_id}): {e}")
|
1022
1017
|
raise e
|
1023
1018
|
|
1024
|
-
|
1025
1019
|
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
1020
|
if not self._session or self._session.closed:
|
1027
1021
|
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
@@ -1368,6 +1362,54 @@ class ArizonaAPI:
|
|
1368
1362
|
print(f"Неожиданная ошибка при поиске тем '{query}': {e}")
|
1369
1363
|
return []
|
1370
1364
|
|
1365
|
+
async def search_members(self, nickname: str) -> list:
|
1366
|
+
"""Поиск пользователей по нику
|
1367
|
+
|
1368
|
+
Attributes:
|
1369
|
+
nickname (str): Никнейм или его часть для поиска
|
1370
|
+
|
1371
|
+
Returns:
|
1372
|
+
Список словарей с информацией о найденных пользователях
|
1373
|
+
"""
|
1374
|
+
if not self._session or self._session.closed:
|
1375
|
+
raise Exception("Сессия не активна. Вызовите connect() сначала.")
|
1376
|
+
|
1377
|
+
try:
|
1378
|
+
token = await self.token
|
1379
|
+
url = f"{MAIN_URL}/index.php?members/find&q={nickname}&_xfRequestUri=%2Fsearch%2F&_xfWithData=1&_xfToken={token}&_xfResponseType=json"
|
1380
|
+
|
1381
|
+
async with self._session.get(url) as response:
|
1382
|
+
response.raise_for_status()
|
1383
|
+
data = await response.json()
|
1384
|
+
|
1385
|
+
results = []
|
1386
|
+
if data.get('results'):
|
1387
|
+
for user in data['results']:
|
1388
|
+
try:
|
1389
|
+
user_id_match = re.search(r'data-user-id="(\d+)"', user.get('username_color', ''))
|
1390
|
+
user_id = int(user_id_match.group(1)) if user_id_match else None
|
1391
|
+
avatar_match = re.search(r'src="([^"]+)"', user.get('iconHtml', ''))
|
1392
|
+
avatar = avatar_match.group(1) if avatar_match else None
|
1393
|
+
|
1394
|
+
user_data = {
|
1395
|
+
'user_id': user_id,
|
1396
|
+
'username': user.get('id'),
|
1397
|
+
'avatar': avatar,
|
1398
|
+
'profile_url': f"{MAIN_URL}{user['username_color'].split('href=\"')[1].split('\"')[0]}" if user.get('username_color') else None
|
1399
|
+
}
|
1400
|
+
results.append(user_data)
|
1401
|
+
except (ValueError, KeyError, AttributeError) as e:
|
1402
|
+
print(f"Ошибка обработки данных пользователя: {e}")
|
1403
|
+
continue
|
1404
|
+
|
1405
|
+
return results
|
1406
|
+
except aiohttp.ClientError as e:
|
1407
|
+
print(f"Ошибка сети при поиске пользователей по нику '{nickname}': {e}")
|
1408
|
+
return []
|
1409
|
+
except Exception as e:
|
1410
|
+
print(f"Неожиданная ошибка при поиске пользователей '{nickname}': {e}")
|
1411
|
+
return []
|
1412
|
+
|
1371
1413
|
async def mark_notifications_read(self, alert_ids: list[int]) -> aiohttp.ClientResponse:
|
1372
1414
|
"""Пометить уведомления как прочитанные"""
|
1373
1415
|
if not self._session or self._session.closed:
|
@@ -1446,4 +1488,359 @@ class ArizonaAPI:
|
|
1446
1488
|
return ''
|
1447
1489
|
except Exception as e:
|
1448
1490
|
print(f"Неожиданная ошибка при конвертации BBCode для поста {post_id}: {e}")
|
1449
|
-
return ''
|
1491
|
+
return ''
|
1492
|
+
|
1493
|
+
async def get_category_statistics_threads(self, category_id: int, duration: str = 'week') -> Optional[Dict]:
|
1494
|
+
"""
|
1495
|
+
Собирает статистику по темам в указанной категории за определенный период.
|
1496
|
+
Останавливает просмотр страниц, как только на странице не будет найдено тем, созданных после начала периода.
|
1497
|
+
|
1498
|
+
Args:
|
1499
|
+
category_id (int): ID категории форума.
|
1500
|
+
duration (str): Период для статистики ('day', 'week', 'month'). По умолчанию 'week'.
|
1501
|
+
|
1502
|
+
Returns:
|
1503
|
+
Optional[Dict]: Словарь со статистикой или None в случае ошибки.
|
1504
|
+
Структура словаря:
|
1505
|
+
{
|
1506
|
+
'category_title': str,
|
1507
|
+
'category_id': int,
|
1508
|
+
'period': str,
|
1509
|
+
'start_timestamp': int,
|
1510
|
+
'end_timestamp': int,
|
1511
|
+
'total_threads_in_category': int, # Общее кол-во тем, обработанных до остановки
|
1512
|
+
'on_review': int, # Открытые и не закрепленные
|
1513
|
+
'pinned': int,
|
1514
|
+
'unpinned': int, # Все не закрепленные (включая 'on_review')
|
1515
|
+
'closed_in_period': int, # Закрытые именно в этот период
|
1516
|
+
'currently_open': int, # Текущее кол-во открытых тем (среди обработанных)
|
1517
|
+
'currently_closed': int, # Текущее кол-во закрытых тем (среди обработанных)
|
1518
|
+
'average_closing_time': str, # Среднее время закрытия в ЧЧ:ММ:СС
|
1519
|
+
'average_closing_time_seconds': float,
|
1520
|
+
'closer_stats': List[Dict], # Список закрывших с кол-вом и процентом
|
1521
|
+
'total_pages_in_category': int, # Общее кол-во страниц в категории
|
1522
|
+
'processed_pages': int # Кол-во фактически обработанных страниц
|
1523
|
+
}
|
1524
|
+
"""
|
1525
|
+
if not self._session or self._session.closed:
|
1526
|
+
print("Ошибка: Сессия не активна. Вызовите connect() сначала.")
|
1527
|
+
return None
|
1528
|
+
|
1529
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
1530
|
+
if duration == 'day':
|
1531
|
+
delta = datetime.timedelta(days=1)
|
1532
|
+
period_str = "день"
|
1533
|
+
elif duration == 'week':
|
1534
|
+
delta = datetime.timedelta(weeks=1)
|
1535
|
+
period_str = "неделю"
|
1536
|
+
elif duration == 'month':
|
1537
|
+
delta = datetime.timedelta(days=30)
|
1538
|
+
period_str = "месяц"
|
1539
|
+
elif duration == 'year':
|
1540
|
+
delta = datetime.timedelta(days=365)
|
1541
|
+
period_str = "год"
|
1542
|
+
else:
|
1543
|
+
print(f"Ошибка: Неверное значение duration '{duration}'. Используйте 'day', 'week' или 'month'.")
|
1544
|
+
return None
|
1545
|
+
|
1546
|
+
start_timestamp = int((now - delta).timestamp())
|
1547
|
+
|
1548
|
+
category_info = await self.get_category(category_id)
|
1549
|
+
if not category_info:
|
1550
|
+
print(f"Не удалось получить информацию о категории {category_id}")
|
1551
|
+
return None
|
1552
|
+
|
1553
|
+
total_pages_in_category = category_info.pages_count
|
1554
|
+
category_title = category_info.title
|
1555
|
+
|
1556
|
+
pinned_count = 0
|
1557
|
+
unpinned_count = 0
|
1558
|
+
currently_closed_count = 0
|
1559
|
+
currently_open_count = 0
|
1560
|
+
on_review_count = 0
|
1561
|
+
|
1562
|
+
closed_in_period_count = 0
|
1563
|
+
total_closing_duration_seconds = 0
|
1564
|
+
closers_stats = defaultdict(int)
|
1565
|
+
|
1566
|
+
all_threads_processed = 0
|
1567
|
+
processed_pages_count = 0
|
1568
|
+
|
1569
|
+
for page in range(1, total_pages_in_category + 1):
|
1570
|
+
processed_pages_count = page
|
1571
|
+
page_contains_recent_threads = False
|
1572
|
+
|
1573
|
+
try:
|
1574
|
+
page_threads = await self.get_thread_category_detail(category_id, page)
|
1575
|
+
|
1576
|
+
if page_threads is None:
|
1577
|
+
print(f"Предупреждение: Не удалось получить или обработать темы со страницы {page} категории {category_id} (возможно, ошибка парсинга). Пропускаем страницу.")
|
1578
|
+
continue
|
1579
|
+
|
1580
|
+
except AttributeError as e:
|
1581
|
+
print(f"Ошибка атрибута при обработке страницы {page} категории {category_id}: {e}. Пропускаем страницу.")
|
1582
|
+
continue
|
1583
|
+
except Exception as e:
|
1584
|
+
print(f"Неожиданная ошибка при получении тем из категории {category_id} (страница {page}): {e}. Пропускаем страницу.")
|
1585
|
+
continue
|
1586
|
+
|
1587
|
+
if not page_threads:
|
1588
|
+
print(f"Страница {page} категории {category_id} пуста или не содержит тем.")
|
1589
|
+
continue
|
1590
|
+
|
1591
|
+
all_threads_processed += len(page_threads)
|
1592
|
+
|
1593
|
+
for thread_data in page_threads:
|
1594
|
+
if thread_data.get('is_pinned'):
|
1595
|
+
pinned_count += 1
|
1596
|
+
else:
|
1597
|
+
unpinned_count += 1
|
1598
|
+
|
1599
|
+
if thread_data.get('is_closed'):
|
1600
|
+
currently_closed_count += 1
|
1601
|
+
else:
|
1602
|
+
currently_open_count += 1
|
1603
|
+
if not thread_data.get('is_pinned'):
|
1604
|
+
on_review_count += 1
|
1605
|
+
|
1606
|
+
last_message_date = thread_data.get('last_message_date')
|
1607
|
+
closer_username = thread_data.get('username_last_message')
|
1608
|
+
created_date = thread_data.get('created_date')
|
1609
|
+
|
1610
|
+
if thread_data.get('is_closed') and last_message_date and closer_username and created_date:
|
1611
|
+
if last_message_date >= start_timestamp:
|
1612
|
+
closed_in_period_count += 1
|
1613
|
+
closing_time_seconds = last_message_date - created_date
|
1614
|
+
if closing_time_seconds >= 0:
|
1615
|
+
total_closing_duration_seconds += closing_time_seconds
|
1616
|
+
closers_stats[closer_username] += 1
|
1617
|
+
|
1618
|
+
if created_date and created_date >= start_timestamp:
|
1619
|
+
page_contains_recent_threads = True
|
1620
|
+
|
1621
|
+
if not page_contains_recent_threads:
|
1622
|
+
print(f"Остановка на странице {page}: не найдено тем, созданных после {datetime.datetime.fromtimestamp(start_timestamp, tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}.")
|
1623
|
+
break
|
1624
|
+
|
1625
|
+
average_closing_time_str = "N/A"
|
1626
|
+
avg_seconds_float = 0.0
|
1627
|
+
if closed_in_period_count > 0:
|
1628
|
+
avg_seconds = total_closing_duration_seconds / closed_in_period_count
|
1629
|
+
avg_seconds_float = avg_seconds
|
1630
|
+
avg_td = datetime.timedelta(seconds=int(avg_seconds))
|
1631
|
+
|
1632
|
+
total_seconds_int = avg_td.days * 86400 + avg_td.seconds
|
1633
|
+
hours, remainder = divmod(total_seconds_int, 3600)
|
1634
|
+
minutes, seconds = divmod(remainder, 60)
|
1635
|
+
average_closing_time_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
1636
|
+
|
1637
|
+
|
1638
|
+
sorted_closers = sorted(closers_stats.items(), key=lambda item: item[1], reverse=True)
|
1639
|
+
formatted_closers = []
|
1640
|
+
total_closed_by_tracked = sum(closers_stats.values())
|
1641
|
+
|
1642
|
+
for username, count in sorted_closers:
|
1643
|
+
percentage = (count / total_closed_by_tracked * 100) if total_closed_by_tracked > 0 else 0
|
1644
|
+
formatted_closers.append({
|
1645
|
+
'username': username,
|
1646
|
+
'count': count,
|
1647
|
+
'percentage': round(percentage, 2)
|
1648
|
+
})
|
1649
|
+
|
1650
|
+
result = {
|
1651
|
+
'category_title': category_title,
|
1652
|
+
'category_id': category_id,
|
1653
|
+
'period': period_str,
|
1654
|
+
'start_timestamp': start_timestamp,
|
1655
|
+
'end_timestamp': int(now.timestamp()),
|
1656
|
+
'total_threads_in_category': all_threads_processed,
|
1657
|
+
'on_review': on_review_count,
|
1658
|
+
'pinned': pinned_count,
|
1659
|
+
'unpinned': unpinned_count,
|
1660
|
+
'closed_in_period': closed_in_period_count,
|
1661
|
+
'currently_open': currently_open_count,
|
1662
|
+
'currently_closed': currently_closed_count,
|
1663
|
+
'average_closing_time': average_closing_time_str,
|
1664
|
+
'average_closing_time_seconds': avg_seconds_float,
|
1665
|
+
'closer_stats': formatted_closers,
|
1666
|
+
'total_pages_in_category': total_pages_in_category,
|
1667
|
+
'processed_pages': processed_pages_count
|
1668
|
+
}
|
1669
|
+
|
1670
|
+
return result
|
1671
|
+
|
1672
|
+
async def get_category_statistics_posts(self, category_id: int, duration: str = 'week') -> Optional[Dict]:
|
1673
|
+
"""
|
1674
|
+
Собирает статистику по постам в темах указанной категории за определенный период.
|
1675
|
+
|
1676
|
+
Args:
|
1677
|
+
category_id (int): ID категории форума.
|
1678
|
+
duration (str): Период для статистики ('day', 'week', 'month', 'year'). По умолчанию 'week'.
|
1679
|
+
|
1680
|
+
Returns:
|
1681
|
+
Optional[Dict]: Словарь со статистикой по постам или None в случае ошибки.
|
1682
|
+
Структура словаря:
|
1683
|
+
{
|
1684
|
+
'category_title': str,
|
1685
|
+
'category_id': int,
|
1686
|
+
'period': str, # Описание периода ('за день', 'за неделю'...)
|
1687
|
+
'start_timestamp': int, # Начало периода (Unix time)
|
1688
|
+
'end_timestamp': int, # Конец периода (Unix time)
|
1689
|
+
'total_threads_checked': int, # Кол-во тем, чьи посты проверялись
|
1690
|
+
'total_posts_in_period': int, # Общее кол-во постов за период
|
1691
|
+
'posts_by_user': List[Dict], # Список пользователей с кол-вом постов и %
|
1692
|
+
# [{'username': str, 'count': int, 'percentage': float}]
|
1693
|
+
'total_category_pages': int, # Общее кол-во страниц в категории
|
1694
|
+
'processed_category_pages': int, # Кол-во обработанных страниц категории
|
1695
|
+
}
|
1696
|
+
"""
|
1697
|
+
if not self._session or self._session.closed:
|
1698
|
+
print("Ошибка: Сессия не активна. Вызовите connect() сначала.")
|
1699
|
+
return None
|
1700
|
+
|
1701
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
1702
|
+
if duration == 'day':
|
1703
|
+
delta = datetime.timedelta(days=1)
|
1704
|
+
period_str = "за день"
|
1705
|
+
elif duration == 'week':
|
1706
|
+
delta = datetime.timedelta(weeks=1)
|
1707
|
+
period_str = "за неделю"
|
1708
|
+
elif duration == 'month':
|
1709
|
+
delta = datetime.timedelta(days=30)
|
1710
|
+
period_str = "за месяц"
|
1711
|
+
elif duration == 'year':
|
1712
|
+
delta = datetime.timedelta(days=365)
|
1713
|
+
period_str = "за год"
|
1714
|
+
else:
|
1715
|
+
print(f"Ошибка: Неверное значение duration '{duration}'. Используйте 'day', 'week', 'month' или 'year'.")
|
1716
|
+
return None
|
1717
|
+
|
1718
|
+
start_timestamp = int((now - delta).timestamp())
|
1719
|
+
end_timestamp = int(now.timestamp())
|
1720
|
+
|
1721
|
+
category_info = await self.get_category(category_id)
|
1722
|
+
if not category_info:
|
1723
|
+
print(f"Не удалось получить информацию о категории {category_id}")
|
1724
|
+
return None
|
1725
|
+
|
1726
|
+
total_category_pages = category_info.pages_count
|
1727
|
+
category_title = category_info.title
|
1728
|
+
|
1729
|
+
posts_by_user = defaultdict(int)
|
1730
|
+
total_posts_in_period = 0
|
1731
|
+
total_threads_checked = 0
|
1732
|
+
processed_category_pages = 0
|
1733
|
+
|
1734
|
+
for cat_page_num in range(1, total_category_pages + 1):
|
1735
|
+
processed_category_pages = cat_page_num
|
1736
|
+
|
1737
|
+
try:
|
1738
|
+
threads_on_page = await self.get_thread_category_detail(category_id, cat_page_num)
|
1739
|
+
|
1740
|
+
if threads_on_page is None:
|
1741
|
+
print(f"Предупреждение: Не удалось получить темы со страницы {cat_page_num} категории {category_id}. Пропуск страницы.")
|
1742
|
+
continue
|
1743
|
+
if not threads_on_page:
|
1744
|
+
continue
|
1745
|
+
|
1746
|
+
except Exception as e:
|
1747
|
+
print(f"Неожиданная ошибка при получении тем из категории {category_id} (страница {cat_page_num}): {e}. Пропуск страницы.")
|
1748
|
+
continue
|
1749
|
+
|
1750
|
+
for thread_data in threads_on_page:
|
1751
|
+
thread_id = thread_data.get('thread_id')
|
1752
|
+
last_message_date = thread_data.get('last_message_date')
|
1753
|
+
|
1754
|
+
if not thread_id:
|
1755
|
+
print(f"Предупреждение: Пропуск темы без ID на стр. {cat_page_num} категории {category_id}.")
|
1756
|
+
continue
|
1757
|
+
|
1758
|
+
if last_message_date and last_message_date < start_timestamp:
|
1759
|
+
continue
|
1760
|
+
|
1761
|
+
total_threads_checked += 1
|
1762
|
+
|
1763
|
+
try:
|
1764
|
+
thread_details = await self.get_thread(thread_id)
|
1765
|
+
if not thread_details:
|
1766
|
+
print(f"Предупреждение: Не удалось получить детали темы {thread_id}. Пропуск темы.")
|
1767
|
+
continue
|
1768
|
+
thread_pages_count = thread_details.pages_count
|
1769
|
+
except Exception as e:
|
1770
|
+
print(f"Ошибка при получении деталей темы {thread_id}: {e}. Пропуск темы.")
|
1771
|
+
continue
|
1772
|
+
|
1773
|
+
stop_processing_this_thread = False
|
1774
|
+
|
1775
|
+
for thread_page_num in range(thread_pages_count, 0, -1):
|
1776
|
+
if stop_processing_this_thread:
|
1777
|
+
break
|
1778
|
+
|
1779
|
+
page_url = f"{MAIN_URL}/threads/{thread_id}/page-{thread_page_num}"
|
1780
|
+
try:
|
1781
|
+
async with self._session.get(page_url) as response:
|
1782
|
+
if response.status == 404:
|
1783
|
+
print(f"Предупреждение: Страница {thread_page_num} темы {thread_id} не найдена (404).")
|
1784
|
+
continue
|
1785
|
+
response.raise_for_status()
|
1786
|
+
page_html = await response.text()
|
1787
|
+
page_soup = BeautifulSoup(page_html, 'lxml')
|
1788
|
+
|
1789
|
+
posts_on_page = page_soup.find_all('article', class_=re.compile(r'\bmessage--post\b'))
|
1790
|
+
if not posts_on_page:
|
1791
|
+
continue
|
1792
|
+
|
1793
|
+
page_had_relevant_posts = False
|
1794
|
+
|
1795
|
+
for post_article in posts_on_page:
|
1796
|
+
post_author_name = "Неизвестный автор"
|
1797
|
+
post_author_tag = post_article.find('a', class_='username', attrs={'data-user-id': True})
|
1798
|
+
if post_author_tag:
|
1799
|
+
post_author_name = post_author_tag.text.strip()
|
1800
|
+
|
1801
|
+
post_timestamp = 0
|
1802
|
+
post_time_tag = post_article.find('time', class_='u-dt', attrs={'data-time': True})
|
1803
|
+
if post_time_tag and post_time_tag.get('data-time','').isdigit():
|
1804
|
+
post_timestamp = int(post_time_tag['data-time'])
|
1805
|
+
else:
|
1806
|
+
continue
|
1807
|
+
|
1808
|
+
if post_timestamp >= start_timestamp:
|
1809
|
+
total_posts_in_period += 1
|
1810
|
+
posts_by_user[post_author_name] += 1
|
1811
|
+
page_had_relevant_posts = True
|
1812
|
+
else:
|
1813
|
+
stop_processing_this_thread = True
|
1814
|
+
break
|
1815
|
+
|
1816
|
+
except aiohttp.ClientError as e:
|
1817
|
+
print(f"Ошибка сети при получении страницы {thread_page_num} темы {thread_id}: {e}")
|
1818
|
+
continue
|
1819
|
+
except Exception as e:
|
1820
|
+
print(f"Неожиданная ошибка при обработке страницы {thread_page_num} темы {thread_id}: {e}")
|
1821
|
+
continue
|
1822
|
+
|
1823
|
+
sorted_users = sorted(posts_by_user.items(), key=lambda item: item[1], reverse=True)
|
1824
|
+
formatted_users = []
|
1825
|
+
for username, count in sorted_users:
|
1826
|
+
percentage = (count / total_posts_in_period * 100) if total_posts_in_period > 0 else 0
|
1827
|
+
formatted_users.append({
|
1828
|
+
'username': username,
|
1829
|
+
'count': count,
|
1830
|
+
'percentage': round(percentage, 2)
|
1831
|
+
})
|
1832
|
+
|
1833
|
+
result = {
|
1834
|
+
'category_title': category_title,
|
1835
|
+
'category_id': category_id,
|
1836
|
+
'period': period_str,
|
1837
|
+
'start_timestamp': start_timestamp,
|
1838
|
+
'end_timestamp': end_timestamp,
|
1839
|
+
'total_threads_checked': total_threads_checked,
|
1840
|
+
'total_posts_in_period': total_posts_in_period,
|
1841
|
+
'posts_by_user': formatted_users,
|
1842
|
+
'total_category_pages': total_category_pages,
|
1843
|
+
'processed_category_pages': processed_category_pages,
|
1844
|
+
}
|
1845
|
+
|
1846
|
+
return result
|
@@ -810,7 +810,6 @@ _0xfab6 = [
|
|
810
810
|
|
811
811
|
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 OPR/86.0.4363.64"
|
812
812
|
|
813
|
-
|
814
813
|
def to_numbers(value):
|
815
814
|
return dukpy.evaljs('''
|
816
815
|
var _0x9ee6x3 = [];
|
@@ -869,7 +868,6 @@ async def bypass_async(agent=user_agent, proxy=""):
|
|
869
868
|
a, b, c = to_numbers(found[0]), to_numbers(found[1]), to_numbers(found[2])
|
870
869
|
return _0xfab6[11] + to_hex([slow_aes([c, a, b]), _0xfab6]), session.headers.get("user-agent")
|
871
870
|
|
872
|
-
|
873
871
|
def main():
|
874
872
|
code = bypass()
|
875
873
|
cookies = "name=value; name=value; name=value; " # Из браузера копируем авторизованные куки без куки react lab arz
|
@@ -878,6 +876,5 @@ def main():
|
|
878
876
|
username = re.compile("<span class=\"p-navgroup-linkText username--.*\">(.*)</span>").findall(r.text)
|
879
877
|
print(username)
|
880
878
|
|
881
|
-
|
882
879
|
if __name__ == '__main__':
|
883
880
|
main()
|
@@ -1,21 +0,0 @@
|
|
1
|
-
MIT License
|
2
|
-
|
3
|
-
Copyright (c) 2025 Mikhail
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
7
|
-
in the Software without restriction, including without limitation the rights
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
10
|
-
furnished to do so, subject to the following conditions:
|
11
|
-
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
13
|
-
copies or substantial portions of the Software.
|
14
|
-
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
SOFTWARE.
|
{arizona_forum_api_async-1.0.dist-info → arizona_forum_api_async-1.1.dist-info}/top_level.txt
RENAMED
File without changes
|