arz_api.api
1from bs4 import BeautifulSoup 2from re import compile, findall 3from requests import session, Response 4from html import unescape 5 6from arz_api.consts import MAIN_URL 7from arz_api.bypass_antibot import bypass 8 9from arz_api.exceptions import IncorrectLoginData, ThisIsYouError 10from arz_api.models.other import Statistic 11from arz_api.models.post_object import Post, ProfilePost 12from arz_api.models.member_object import Member, CurrentMember 13from arz_api.models.thread_object import Thread 14from arz_api.models.category_object import Category 15 16 17class ArizonaAPI: 18 def __init__(self, user_agent: str, cookie: dict, do_bypass: bool = True) -> None: 19 self.user_agent = user_agent 20 self.cookie = cookie 21 self.session = session() 22 self.session.headers = {"user-agent": user_agent} 23 self.session.cookies.update(cookie) 24 25 if do_bypass: 26 name, code = str(bypass(user_agent)).split('=') 27 self.session.cookies.set(name, code) 28 29 if BeautifulSoup(self.session.get(f"{MAIN_URL}").content, 'lxml').find('html')['data-logged-in'] == "false": 30 raise IncorrectLoginData 31 32 33 def logout(self): 34 """Закрыть сессию""" 35 return self.session.close() 36 37 38 @property 39 def current_member(self) -> CurrentMember: 40 """Объект текущего пользователя""" 41 42 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/account").content, 'lxml') 43 user_id = int(content.find('span', {'class': 'avatar--xxs'})['data-user-id']) 44 member_info = self.get_member(user_id) 45 46 return CurrentMember(self, user_id, member_info.username, member_info.user_title, member_info.avatar, member_info.roles, member_info.messages_count, member_info.reactions_count, member_info.trophies_count) 47 48 @property 49 def token(self) -> str: 50 """Токен CSRF""" 51 return BeautifulSoup(self.session.get(f"{MAIN_URL}/help/terms/").content, 'lxml').find('html')['data-csrf'] 52 53 54 def get_category(self, category_id: int) -> Category: 55 """Найти раздел по ID""" 56 57 request = self.session.get(f"{MAIN_URL}/forums/{category_id}?_xfResponseType=json&_xfToken={self.token}").json() 58 if request['status'] == 'error': 59 return None 60 61 content = unescape(request['html']['content']) 62 content = BeautifulSoup(content, 'lxml') 63 64 title = unescape(request['html']['title']) 65 try: pages_count = int(content.find_all('li', {'class': 'pageNav-page'})[-1].text) 66 except IndexError: pages_count = 1 67 68 return Category(self, category_id, title, pages_count) 69 70 71 def get_member(self, user_id: int) -> Member: 72 """Найти пользователя по ID (возвращает либо Member, либо None (если профиль закрыт / не существует))""" 73 74 request = self.session.get(f"{MAIN_URL}/members/{user_id}?_xfResponseType=json&_xfToken={self.token}").json() 75 if request['status'] == 'error': 76 return None 77 78 content = unescape(request['html']['content']) 79 content = BeautifulSoup(content, 'lxml') 80 81 username = unescape(request['html']['title']) 82 83 roles = [] 84 for i in content.find('div', {'class': 'memberHeader-banners'}).children: 85 if i.text != '\n': roles.append(i.text) 86 87 try: user_title = content.find('span', {'class': 'userTitle'}).text 88 except AttributeError: user_title = None 89 try: avatar = MAIN_URL + content.find('a', {'class': 'avatar avatar--l'})['href'] 90 except TypeError: avatar = None 91 92 messages_count = int(content.find('a', {'href': f'/search/member?user_id={user_id}'}).text.strip().replace(',', '')) 93 reactions_count = int(content.find('dl', {'class': 'pairs pairs--rows pairs--rows--centered'}).find('dd').text.strip().replace(',', '')) 94 trophies_count = int(content.find('a', {'href': f'/members/{user_id}/trophies'}).text.strip().replace(',', '')) 95 96 return Member(self, user_id, username, user_title, avatar, roles, messages_count, reactions_count, trophies_count) 97 98 99 def get_thread(self, thread_id: int) -> Thread: 100 """Найти тему по ID""" 101 request = self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1?_xfResponseType=json&_xfToken={self.token}").json() 102 if request['status'] == 'error': 103 return None 104 105 if request.get('redirect') is not None: 106 return self.get_thread(request['redirect'].split(MAIN_URL, maxsplit=1)[-1].split('/')[1]) 107 108 content = BeautifulSoup(unescape(request['html']['content']), 'lxml') 109 content_h1 = BeautifulSoup(unescape(request['html']['h1']), 'lxml') 110 111 creator_id = content.find('a', {'class': 'username'}) 112 try: creator = self.get_member(int(creator_id['data-user-id'])) 113 except: creator = Member(self, int(creator_id['data-user-id']), content.find('a', {'class': 'username'}).text, None, None, None, None, None, None) 114 115 create_date = int(content.find('time')['data-time']) 116 117 try: 118 prefix = content_h1.find('span', {'class': 'label'}).text 119 title = content_h1.text.strip().replace(prefix, "").strip() 120 121 except AttributeError: 122 prefix = "" 123 title = content_h1.text 124 125 thread_content_html = content.find('div', {'class': 'bbWrapper'}) 126 thread_content = thread_content_html.text 127 128 try: pages_count = int(content.find_all('li', {'class': 'pageNav-page'})[-1].text) 129 except IndexError: pages_count = 1 130 131 is_closed = False 132 if content.find('dl', {'class': 'blockStatus'}): is_closed = True 133 thread_post_id = content.find('article', {'id': compile('js-post-*')})['id'].split('js-post-', maxsplit=1)[-1] 134 135 return Thread(self, thread_id, creator, create_date, title, prefix, thread_content, thread_content_html, pages_count, thread_post_id, is_closed) 136 137 138 def get_post(self, post_id: int) -> Post: 139 """Найти пост по ID (Post если существует, None - удален / нет доступа)""" 140 141 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/posts/{post_id}").content, 'lxml') 142 post = content.find('article', {'id': f'js-post-{post_id}'}) 143 if post is None: 144 return None 145 146 try: creator = self.get_member(int(post.find('a', {'data-xf-init': 'member-tooltip'})['data-user-id'])) 147 except: 148 user_info = post.find('a', {'data-xf-init': 'member-tooltip'}) 149 creator = Member(self, int(user_info['data-user-id']), user_info.text, None, None, None, None, None, None) 150 151 thread = self.get_thread(int(content.find('html')['data-content-key'].split('-')[-1])) 152 create_date = int(post.find('time', {'class': 'u-dt'})['data-time']) 153 bb_content = post.find('div', {'class': 'bbWrapper'}) 154 text_content = bb_content.text 155 return Post(self, post_id, creator, thread, create_date, bb_content, text_content) 156 157 158 def get_profile_post(self, post_id: int) -> ProfilePost: 159 """Найти сообщение профиля по ID""" 160 161 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/profile-posts/{post_id}").content, 'lxml') 162 post = content.find('article', {'id': f'js-profilePost-{post_id}'}) 163 if post is None: 164 return None 165 166 creator = self.get_member(int(post.find('a', {'class': 'username'})['data-user-id'])) 167 profile = self.get_member(int(content.find('span', {'class': 'username'})['data-user-id'])) 168 create_date = int(post.find('time')['data-time']) 169 bb_content = post.find('div', {'class': 'bbWrapper'}) 170 text_content = bb_content.text 171 172 return ProfilePost(self, post_id, creator, profile, create_date, bb_content, text_content) 173 174 def get_forum_statistic(self) -> Statistic: 175 """Получить статистику форума""" 176 177 content = BeautifulSoup(self.session.get(MAIN_URL).content, 'lxml') 178 threads_count = int(content.find('dl', {'class': 'pairs pairs--justified count--threads'}).find('dd').text.replace(',', '')) 179 posts_count = int(content.find('dl', {'class': 'pairs pairs--justified count--messages'}).find('dd').text.replace(',', '')) 180 users_count = int(content.find('dl', {'class': 'pairs pairs--justified count--users'}).find('dd').text.replace(',', '')) 181 last_register_member = self.get_member(int(content.find('dl', {'class': 'pairs pairs--justified'}).find('a')['data-user-id'])) 182 183 return Statistic(self, threads_count, posts_count, users_count, last_register_member) 184 185 186 # ---------------================ МЕТОДЫ ОБЪЕКТОВ ====================-------------------- 187 188 189 # CATEGORY 190 def create_thread(self, category_id: int, title: str, message_html: str, discussion_type: str = 'discussion', watch_thread: bool = True) -> Response: 191 """Создать тему в категории 192 193 Attributes: 194 category_id (int): ID категории 195 title (str): Название темы 196 message_html (str): Содержание темы. Рекомендуется использование HTML 197 discussion_type (str): (необяз.) Тип темы | Возможные варианты: 'discussion' - обсуждение (по умолчанию), 'article' - статья, 'poll' - опрос 198 watch_thread (str): (необяз.) Отслеживать ли тему. По умолчанию True 199 200 Returns: 201 Объект Response модуля requests 202 203 Todo: 204 Cделать возврат ID новой темы 205 """ 206 207 return self.session.post(f"{MAIN_URL}/forums/{category_id}/post-thread?inline-mode=1", {'_xfToken': self.token, 'title': title, 'message_html': message_html, 'discussion_type': discussion_type, 'watch_thread': int(watch_thread)}) 208 209 210 def set_read_category(self, category_id: int) -> Response: 211 """Отметить категорию как прочитанную 212 213 Attributes: 214 category_id (int): ID категории 215 216 Returns: 217 Объект Response модуля requests 218 """ 219 220 return self.session.post(f"{MAIN_URL}/forums/{category_id}/mark-read", {'_xfToken': self.token}) 221 222 223 def watch_category(self, category_id: int, notify: str, send_alert: bool = True, send_email: bool = False, stop: bool = False) -> Response: 224 """Настроить отслеживание категории 225 226 Attributes: 227 category_id (int): ID категории 228 notify (str): Объект отслеживания. Возможные варианты: "thread", "message", "" 229 send_alert (bool): (необяз.) Отправлять ли уведомления на форуме. По умолчанию True 230 send_email (bool): (необяз.) Отправлять ли уведомления на почту. По умолчанию False 231 stop (bool): (необяз.) Принудительное завершение отслеживания. По умолчанию False 232 233 Returns: 234 Объект Response модуля requests 235 """ 236 237 if stop: return self.session.post(f"{MAIN_URL}/forums/{category_id}/watch", {'_xfToken': self.token, 'stop': "1"}) 238 else: return self.session.post(f"{MAIN_URL}/forums/{category_id}/watch", {'_xfToken': self.token, 'send_alert': int(send_alert), 'send_email': int(send_email), 'notify': notify}) 239 240 241 def get_category_threads(self, category_id: int, page: int = 1) -> dict: 242 """Получить темы из раздела 243 244 Attributes: 245 category_id (int): ID категории 246 page (int): (необяз.) Cтраница для поиска. По умолчанию 1 247 248 Returns: 249 Словарь (dict) по структуре THREAD_ID : 'pin'/'unpin' 250 """ 251 252 request = self.session.get(f"{MAIN_URL}/forums/{category_id}/page-{page}?_xfResponseType=json&_xfToken={self.token}").json() 253 if request['status'] == 'error': 254 return None 255 256 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 257 result = {} 258 for thread in soup.find_all('div', compile('structItem structItem--thread.*')): 259 link = thread.find_all('div', "structItem-title")[0].find_all("a")[-1] 260 261 thread_id = findall(r'\d+', link['href']) # 262 if len(thread_id) < 1: continue 263 thread_id = int(thread_id[0]) 264 265 template = {thread_id: 'unpin'} 266 if len(thread.find_all('i', {'title': 'Закреплено'})) > 0: template[thread_id] = 'pin' 267 268 result.update(template) 269 270 return result 271 272 273 def get_parent_category_of_category(self, category_id: int) -> Category: 274 """Получить родительский раздел раздела 275 276 Attributes: 277 category_id (int): ID категории 278 279 Returns: 280 - Если существует: Объект Catrgory, в котором создан раздел 281 - Если не существует: None 282 """ 283 284 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/forums/{category_id}").content, 'lxml') 285 286 parent_category_id = str(content.find('ul', {'class': 'p-breadcrumbs'}).find_all('li')[-1].find('a')['href'].split('/')[2]) 287 if not parent_category_id.isdigit(): 288 return None 289 290 return self.get_category(parent_category_id) 291 292 293 def get_categories(self, category_id: int) -> list: 294 """Получить дочерние категории из раздела 295 296 Attributes: 297 category_id (int): ID категории 298 299 Returns: 300 Список (list), состоящий из ID дочерних категорий раздела 301 """ 302 303 request = self.session.get(f"{MAIN_URL}/forums/{category_id}/page-1?_xfResponseType=json&_xfToken={self.token}").json() 304 if request['status'] == 'error': 305 return None 306 307 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 308 return [int(findall(r'\d+', category.find("a")['href'])[0]) for category in soup.find_all('div', compile('.*node--depth2 node--forum.*'))] 309 310 311 # MEMBER 312 def follow_member(self, member_id: int) -> Response: 313 """Изменить статус подписки на пользователя 314 315 Attributes: 316 member_id (int): ID пользователя 317 318 Returns: 319 Объект Response модуля requests 320 """ 321 322 if member_id == self.current_member.id: 323 raise ThisIsYouError(member_id) 324 325 return self.session.post(f"{MAIN_URL}/members/{member_id}/follow", {'_xfToken': self.token}) 326 327 328 def ignore_member(self, member_id: int) -> Response: 329 """Изменить статус игнорирования пользователя 330 331 Attributes: 332 member_id (int): ID пользователя 333 334 Returns: 335 Объект Response модуля requests 336 """ 337 338 if member_id == self.current_member.id: 339 raise ThisIsYouError(member_id) 340 341 return self.session.post(f"{MAIN_URL}/members/{member_id}/ignore", {'_xfToken': self.token}) 342 343 344 def add_profile_message(self, member_id: int, message_html: str) -> Response: 345 """Отправить сообщение на стенку пользователя 346 347 Attributes: 348 member_id (int): ID пользователя 349 message_html (str): Текст сообщения. Рекомендуется использование HTML 350 351 Returns: 352 Объект Response модуля requests 353 """ 354 355 return self.session.post(f"{MAIN_URL}/members/{member_id}/post", {'_xfToken': self.token, 'message_html': message_html}) 356 357 358 def get_profile_messages(self, member_id: int, page: int = 1) -> list | None: 359 """Возвращает ID всех сообщений со стенки пользователя на странице 360 361 Attributes: 362 member_id (int): ID пользователя 363 page (int): (необяз.) Страница для поиска. По умолчанию 1 364 365 Returns: 366 - Cписок (list) с ID всех сообщений профиля 367 - None, если пользователя не существует / закрыл профиль 368 """ 369 370 request = self.session.get(f"{MAIN_URL}/members/{member_id}/page-{page}?_xfResponseType=json&_xfToken={self.token}").json() 371 if request['status'] == 'error': 372 return None 373 374 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 375 return [int(post['id'].split('-')[2]) for post in soup.find_all('article', {'id': compile('js-profilePost-*')})] 376 377 378 # POST 379 def react_post(self, post_id: int, reaction_id: int = 1) -> Response: 380 """Поставить реакцию на сообщение 381 382 Attributes: 383 post_id (int): ID сообщения 384 reaction_id (int): (необяз.) ID реакции. По умолчанию 1 385 386 Returns: 387 Объект Response модуля requests 388 """ 389 390 return self.session.post(f'{MAIN_URL}/posts/{post_id}/react?reaction_id={reaction_id}', {'_xfToken': self.token}) 391 392 393 def edit_post(self, post_id: int, message_html: str) -> Response: 394 """Отредактировать сообщение 395 396 Attributes: 397 post_id (int): ID сообщения 398 message_html (str): Новый текст сообщения. Рекомендуется использование HTML 399 400 Returns: 401 Объект Response модуля requests 402 """ 403 404 title_of_thread_post = self.get_post(post_id).thread.title 405 return self.session.post(f"{MAIN_URL}/posts/{post_id}/edit", {"title": title_of_thread_post, "message_html": message_html, "message": message_html, "_xfToken": self.token}) 406 407 408 def delete_post(self, post_id: int, reason: str, hard_delete: bool = False) -> Response: 409 """Удалить сообщение 410 411 Attributes: 412 post_id (int): ID сообщения 413 reason (str): Причина для удаления 414 hard_delete (bool): (необяз.) Полное удаление сообщения. По умолчанию False 415 416 Returns: 417 Объект Response модуля requests 418 """ 419 420 return self.session.post(f"{MAIN_URL}/posts/{post_id}/delete", {"reason": reason, "hard_delete": int(hard_delete), "_xfToken": self.token}) 421 422 423 def bookmark_post(self, post_id: int) -> Response: 424 """Добавить сообщение в закладки 425 426 Attributes: 427 post_id (int): ID сообщения 428 429 Returns: 430 Объект Response модуля requests 431 """ 432 433 return self.session.post(f"{MAIN_URL}/posts/{post_id}/bookmark", {"_xfToken": self.token}) 434 435 436 # PROFILE POST 437 def react_profile_post(self, post_id: int, reaction_id: int = 1) -> Response: 438 """Поставить реакцию на сообщение профиля 439 440 Attributes: 441 post_id (int): ID сообщения профиля 442 reaction_id (int): (необяз.) ID реакции. По умолчанию 1 443 444 Returns: 445 Объект Response модуля requests 446 """ 447 448 return self.session.post(f'{MAIN_URL}/profile-posts/{post_id}/react?reaction_id={reaction_id}', {'_xfToken': self.token}) 449 450 451 def comment_profile_post(self, post_id: int, message_html: str) -> Response: 452 """Прокомментировать сообщение профиля 453 454 Attributes: 455 post_id (int): ID сообщения 456 message_html (str): Текст комментария. Рекомендуется использование HTML 457 458 Returns: 459 Объект Response модуля requests 460 """ 461 462 return self.session.post(f"{MAIN_URL}/profile-posts/{post_id}/add-comment", {"message_html": message_html, "_xfToken": self.token}) 463 464 465 def delete_profile_post(self, post_id: int, reason: str, hard_delete: bool = False) -> Response: 466 """Удалить сообщение профиля 467 468 Attributes: 469 post_id (int): ID сообщения профиля 470 reason (str): Причина для удаления 471 hard_delete (bool): Полное удаление сообщения. По умолчанию False (необяз.) 472 473 Returns: 474 Объект Response модуля requests 475 """ 476 477 return self.session.post(f"{MAIN_URL}/profile-posts/{post_id}/delete", {"reason": reason, "hard_delete": int(hard_delete), "_xfToken": self.token}) 478 479 480 def edit_profile_post(self, post_id: int, message_html: str) -> Response: 481 """Отредактировать сообщение профиля 482 483 Attributes: 484 post_id (int): ID сообщения 485 message_html (str): Новый текст сообщения. Рекомендуется использование HTML 486 487 Returns: 488 Объект Response модуля requests 489 """ 490 491 return self.session.post(f"{MAIN_URL}/profile-posts/{post_id}/edit", {"message_html": message_html, "message": message_html, "_xfToken": self.token}) 492 493 494 # THREAD 495 def answer_thread(self, thread_id: int, message_html: str) -> Response: 496 """Оставить сообщенме в теме 497 498 Attributes: 499 thread_id (int): ID темы 500 message_html (str): Текст сообщения. Рекомендуется использование HTML 501 502 Returns: 503 Объект Response модуля requests 504 """ 505 506 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/add-reply", {'_xfToken': self.token, 'message_html': message_html}) 507 508 509 def watch_thread(self, thread_id: int, email_subscribe: bool = False, stop: bool = False) -> Response: 510 """Изменить статус отслеживания темы 511 512 Attributes: 513 thread_id (int): ID темы 514 email_subscribe (bool): (необяз.) Отправлять ли уведомления на почту. По умолчанию False 515 stop (bool): - (необяз.) Принудительно прекратить отслеживание. По умолчанию False 516 517 Returns: 518 Объект Response модуля requests 519 """ 520 521 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/watch", {'_xfToken': self.token, 'stop': int(stop), 'email_subscribe': int(email_subscribe)}) 522 523 524 def delete_thread(self, thread_id: int, reason: str, hard_delete: bool = False) -> Response: 525 """Удалить тему 526 527 Attributes: 528 thread_id (int): ID темы 529 reason (str): Причина для удаления 530 hard_delete (bool): (необяз.) Полное удаление сообщения. По умолчанию False 531 532 Returns: 533 Объект Response модуля requests 534 """ 535 536 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/delete", {"reason": reason, "hard_delete": int(hard_delete), "_xfToken": self.token}) 537 538 539 def edit_thread(self, thread_id: int, message_html: str) -> Response: 540 """Отредактировать содержимое темы 541 542 Attributes: 543 thread_id (int): ID темы 544 message_html (str): Новое содержимое ответа. Рекомендуется использование HTML 545 546 Returns: 547 Объект Response модуля requests 548 """ 549 550 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1").content, 'lxml') 551 thread_post_id = content.find('article', {'id': compile('js-post-*')})['id'].split('js-post-') 552 return self.session.post(f"{MAIN_URL}/posts/{thread_post_id}/edit", {"message_html": message_html, "message": message_html, "_xfToken": self.token}) 553 554 555 def edit_thread_info(self, thread_id: int, title: str, prefix_id: int = None, sticky: bool = True, opened: bool = True) -> Response: 556 """Изменить статус темы, ее префикс и название 557 558 Attributes: 559 thread_id (int): ID темы 560 title (str): Новое название 561 prefix_id (int): Новый ID префикса 562 sticky (bool): Закрепить (True - закреп / False - не закреп) 563 opened (bool): Открыть/закрыть тему (True - открыть / False - закрыть) 564 565 Returns: 566 Объект Response модуля requests 567 """ 568 569 data = {"_xfToken": self.token, 'title': title} 570 571 if prefix_id is not None: data.update({'prefix_id': prefix_id}) 572 if opened: data.update({"discussion_open": 1}) 573 if sticky: data.update({"sticky": 1}) 574 575 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/edit", data) 576 577 578 def get_thread_category(self, thread_id: int) -> Category: 579 """Получить объект раздела, в котором создана тема 580 581 Attributes: 582 thread_id (int): ID темы 583 584 Returns: 585 Объект Catrgory, в котормо создана тема 586 """ 587 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1").content, 'lxml') 588 589 creator_id = content.find('a', {'class': 'username'}) 590 if creator_id is None: return None 591 592 return self.get_category(int(content.find('html')['data-container-key'].strip('node-'))) 593 594 595 def get_thread_posts(self, thread_id: int, page: int = 1) -> list: 596 """Получить все сообщения из темы на странице 597 598 Attributes: 599 thread_id (int): ID темы 600 page (int): (необяз.) Cтраница для поиска. По умолчанию 1 601 602 Returns: 603 Список (list), состоящий из ID всех сообщений на странице 604 """ 605 606 request = self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-{page}?_xfResponseType=json&_xfToken={self.token}").json() 607 if request['status'] == 'error': 608 return None 609 610 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 611 return [i['id'].strip('js-post-') for i in soup.find_all('article', {'id': compile('js-post-*')})] 612 613 614 def react_thread(self, thread_id: int, reaction_id: int = 1) -> Response: 615 """Поставить реакцию на тему 616 617 Attributes: 618 thread_id (int): ID темы 619 reaction_id (int): (необяз.) ID реакции. По умолчанию 1 620 621 Returns: 622 Объект Response модуля requests 623 """ 624 625 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1").content, 'lxml') 626 thread_post_id = content.find('article', {'id': compile('js-post-*')})['id'].strip('js-post-') 627 return self.session.post(f'{MAIN_URL}/posts/{thread_post_id}/react?reaction_id={reaction_id}', {'_xfToken': self.token}) 628 629 630 # OTHER 631 def send_form(self, form_id: int, data: dict) -> Response: 632 """Заполнить форму 633 634 Attributes: 635 form_id (int): ID формы 636 data (dict): Информация для запонения в виде словаря. Форма словаря: {'question[id вопроса]' = 'необходимая информация'} | Пример: {'question[531]' = '1'} 637 638 Returns: 639 Объект Response модуля requests 640 """ 641 642 data.update({'_xfToken': self.token}) 643 return self.session.post(f"{MAIN_URL}/form/{form_id}/submit", data)
18class ArizonaAPI: 19 def __init__(self, user_agent: str, cookie: dict, do_bypass: bool = True) -> None: 20 self.user_agent = user_agent 21 self.cookie = cookie 22 self.session = session() 23 self.session.headers = {"user-agent": user_agent} 24 self.session.cookies.update(cookie) 25 26 if do_bypass: 27 name, code = str(bypass(user_agent)).split('=') 28 self.session.cookies.set(name, code) 29 30 if BeautifulSoup(self.session.get(f"{MAIN_URL}").content, 'lxml').find('html')['data-logged-in'] == "false": 31 raise IncorrectLoginData 32 33 34 def logout(self): 35 """Закрыть сессию""" 36 return self.session.close() 37 38 39 @property 40 def current_member(self) -> CurrentMember: 41 """Объект текущего пользователя""" 42 43 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/account").content, 'lxml') 44 user_id = int(content.find('span', {'class': 'avatar--xxs'})['data-user-id']) 45 member_info = self.get_member(user_id) 46 47 return CurrentMember(self, user_id, member_info.username, member_info.user_title, member_info.avatar, member_info.roles, member_info.messages_count, member_info.reactions_count, member_info.trophies_count) 48 49 @property 50 def token(self) -> str: 51 """Токен CSRF""" 52 return BeautifulSoup(self.session.get(f"{MAIN_URL}/help/terms/").content, 'lxml').find('html')['data-csrf'] 53 54 55 def get_category(self, category_id: int) -> Category: 56 """Найти раздел по ID""" 57 58 request = self.session.get(f"{MAIN_URL}/forums/{category_id}?_xfResponseType=json&_xfToken={self.token}").json() 59 if request['status'] == 'error': 60 return None 61 62 content = unescape(request['html']['content']) 63 content = BeautifulSoup(content, 'lxml') 64 65 title = unescape(request['html']['title']) 66 try: pages_count = int(content.find_all('li', {'class': 'pageNav-page'})[-1].text) 67 except IndexError: pages_count = 1 68 69 return Category(self, category_id, title, pages_count) 70 71 72 def get_member(self, user_id: int) -> Member: 73 """Найти пользователя по ID (возвращает либо Member, либо None (если профиль закрыт / не существует))""" 74 75 request = self.session.get(f"{MAIN_URL}/members/{user_id}?_xfResponseType=json&_xfToken={self.token}").json() 76 if request['status'] == 'error': 77 return None 78 79 content = unescape(request['html']['content']) 80 content = BeautifulSoup(content, 'lxml') 81 82 username = unescape(request['html']['title']) 83 84 roles = [] 85 for i in content.find('div', {'class': 'memberHeader-banners'}).children: 86 if i.text != '\n': roles.append(i.text) 87 88 try: user_title = content.find('span', {'class': 'userTitle'}).text 89 except AttributeError: user_title = None 90 try: avatar = MAIN_URL + content.find('a', {'class': 'avatar avatar--l'})['href'] 91 except TypeError: avatar = None 92 93 messages_count = int(content.find('a', {'href': f'/search/member?user_id={user_id}'}).text.strip().replace(',', '')) 94 reactions_count = int(content.find('dl', {'class': 'pairs pairs--rows pairs--rows--centered'}).find('dd').text.strip().replace(',', '')) 95 trophies_count = int(content.find('a', {'href': f'/members/{user_id}/trophies'}).text.strip().replace(',', '')) 96 97 return Member(self, user_id, username, user_title, avatar, roles, messages_count, reactions_count, trophies_count) 98 99 100 def get_thread(self, thread_id: int) -> Thread: 101 """Найти тему по ID""" 102 request = self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1?_xfResponseType=json&_xfToken={self.token}").json() 103 if request['status'] == 'error': 104 return None 105 106 if request.get('redirect') is not None: 107 return self.get_thread(request['redirect'].split(MAIN_URL, maxsplit=1)[-1].split('/')[1]) 108 109 content = BeautifulSoup(unescape(request['html']['content']), 'lxml') 110 content_h1 = BeautifulSoup(unescape(request['html']['h1']), 'lxml') 111 112 creator_id = content.find('a', {'class': 'username'}) 113 try: creator = self.get_member(int(creator_id['data-user-id'])) 114 except: creator = Member(self, int(creator_id['data-user-id']), content.find('a', {'class': 'username'}).text, None, None, None, None, None, None) 115 116 create_date = int(content.find('time')['data-time']) 117 118 try: 119 prefix = content_h1.find('span', {'class': 'label'}).text 120 title = content_h1.text.strip().replace(prefix, "").strip() 121 122 except AttributeError: 123 prefix = "" 124 title = content_h1.text 125 126 thread_content_html = content.find('div', {'class': 'bbWrapper'}) 127 thread_content = thread_content_html.text 128 129 try: pages_count = int(content.find_all('li', {'class': 'pageNav-page'})[-1].text) 130 except IndexError: pages_count = 1 131 132 is_closed = False 133 if content.find('dl', {'class': 'blockStatus'}): is_closed = True 134 thread_post_id = content.find('article', {'id': compile('js-post-*')})['id'].split('js-post-', maxsplit=1)[-1] 135 136 return Thread(self, thread_id, creator, create_date, title, prefix, thread_content, thread_content_html, pages_count, thread_post_id, is_closed) 137 138 139 def get_post(self, post_id: int) -> Post: 140 """Найти пост по ID (Post если существует, None - удален / нет доступа)""" 141 142 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/posts/{post_id}").content, 'lxml') 143 post = content.find('article', {'id': f'js-post-{post_id}'}) 144 if post is None: 145 return None 146 147 try: creator = self.get_member(int(post.find('a', {'data-xf-init': 'member-tooltip'})['data-user-id'])) 148 except: 149 user_info = post.find('a', {'data-xf-init': 'member-tooltip'}) 150 creator = Member(self, int(user_info['data-user-id']), user_info.text, None, None, None, None, None, None) 151 152 thread = self.get_thread(int(content.find('html')['data-content-key'].split('-')[-1])) 153 create_date = int(post.find('time', {'class': 'u-dt'})['data-time']) 154 bb_content = post.find('div', {'class': 'bbWrapper'}) 155 text_content = bb_content.text 156 return Post(self, post_id, creator, thread, create_date, bb_content, text_content) 157 158 159 def get_profile_post(self, post_id: int) -> ProfilePost: 160 """Найти сообщение профиля по ID""" 161 162 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/profile-posts/{post_id}").content, 'lxml') 163 post = content.find('article', {'id': f'js-profilePost-{post_id}'}) 164 if post is None: 165 return None 166 167 creator = self.get_member(int(post.find('a', {'class': 'username'})['data-user-id'])) 168 profile = self.get_member(int(content.find('span', {'class': 'username'})['data-user-id'])) 169 create_date = int(post.find('time')['data-time']) 170 bb_content = post.find('div', {'class': 'bbWrapper'}) 171 text_content = bb_content.text 172 173 return ProfilePost(self, post_id, creator, profile, create_date, bb_content, text_content) 174 175 def get_forum_statistic(self) -> Statistic: 176 """Получить статистику форума""" 177 178 content = BeautifulSoup(self.session.get(MAIN_URL).content, 'lxml') 179 threads_count = int(content.find('dl', {'class': 'pairs pairs--justified count--threads'}).find('dd').text.replace(',', '')) 180 posts_count = int(content.find('dl', {'class': 'pairs pairs--justified count--messages'}).find('dd').text.replace(',', '')) 181 users_count = int(content.find('dl', {'class': 'pairs pairs--justified count--users'}).find('dd').text.replace(',', '')) 182 last_register_member = self.get_member(int(content.find('dl', {'class': 'pairs pairs--justified'}).find('a')['data-user-id'])) 183 184 return Statistic(self, threads_count, posts_count, users_count, last_register_member) 185 186 187 # ---------------================ МЕТОДЫ ОБЪЕКТОВ ====================-------------------- 188 189 190 # CATEGORY 191 def create_thread(self, category_id: int, title: str, message_html: str, discussion_type: str = 'discussion', watch_thread: bool = True) -> Response: 192 """Создать тему в категории 193 194 Attributes: 195 category_id (int): ID категории 196 title (str): Название темы 197 message_html (str): Содержание темы. Рекомендуется использование HTML 198 discussion_type (str): (необяз.) Тип темы | Возможные варианты: 'discussion' - обсуждение (по умолчанию), 'article' - статья, 'poll' - опрос 199 watch_thread (str): (необяз.) Отслеживать ли тему. По умолчанию True 200 201 Returns: 202 Объект Response модуля requests 203 204 Todo: 205 Cделать возврат ID новой темы 206 """ 207 208 return self.session.post(f"{MAIN_URL}/forums/{category_id}/post-thread?inline-mode=1", {'_xfToken': self.token, 'title': title, 'message_html': message_html, 'discussion_type': discussion_type, 'watch_thread': int(watch_thread)}) 209 210 211 def set_read_category(self, category_id: int) -> Response: 212 """Отметить категорию как прочитанную 213 214 Attributes: 215 category_id (int): ID категории 216 217 Returns: 218 Объект Response модуля requests 219 """ 220 221 return self.session.post(f"{MAIN_URL}/forums/{category_id}/mark-read", {'_xfToken': self.token}) 222 223 224 def watch_category(self, category_id: int, notify: str, send_alert: bool = True, send_email: bool = False, stop: bool = False) -> Response: 225 """Настроить отслеживание категории 226 227 Attributes: 228 category_id (int): ID категории 229 notify (str): Объект отслеживания. Возможные варианты: "thread", "message", "" 230 send_alert (bool): (необяз.) Отправлять ли уведомления на форуме. По умолчанию True 231 send_email (bool): (необяз.) Отправлять ли уведомления на почту. По умолчанию False 232 stop (bool): (необяз.) Принудительное завершение отслеживания. По умолчанию False 233 234 Returns: 235 Объект Response модуля requests 236 """ 237 238 if stop: return self.session.post(f"{MAIN_URL}/forums/{category_id}/watch", {'_xfToken': self.token, 'stop': "1"}) 239 else: return self.session.post(f"{MAIN_URL}/forums/{category_id}/watch", {'_xfToken': self.token, 'send_alert': int(send_alert), 'send_email': int(send_email), 'notify': notify}) 240 241 242 def get_category_threads(self, category_id: int, page: int = 1) -> dict: 243 """Получить темы из раздела 244 245 Attributes: 246 category_id (int): ID категории 247 page (int): (необяз.) Cтраница для поиска. По умолчанию 1 248 249 Returns: 250 Словарь (dict) по структуре THREAD_ID : 'pin'/'unpin' 251 """ 252 253 request = self.session.get(f"{MAIN_URL}/forums/{category_id}/page-{page}?_xfResponseType=json&_xfToken={self.token}").json() 254 if request['status'] == 'error': 255 return None 256 257 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 258 result = {} 259 for thread in soup.find_all('div', compile('structItem structItem--thread.*')): 260 link = thread.find_all('div', "structItem-title")[0].find_all("a")[-1] 261 262 thread_id = findall(r'\d+', link['href']) # 263 if len(thread_id) < 1: continue 264 thread_id = int(thread_id[0]) 265 266 template = {thread_id: 'unpin'} 267 if len(thread.find_all('i', {'title': 'Закреплено'})) > 0: template[thread_id] = 'pin' 268 269 result.update(template) 270 271 return result 272 273 274 def get_parent_category_of_category(self, category_id: int) -> Category: 275 """Получить родительский раздел раздела 276 277 Attributes: 278 category_id (int): ID категории 279 280 Returns: 281 - Если существует: Объект Catrgory, в котором создан раздел 282 - Если не существует: None 283 """ 284 285 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/forums/{category_id}").content, 'lxml') 286 287 parent_category_id = str(content.find('ul', {'class': 'p-breadcrumbs'}).find_all('li')[-1].find('a')['href'].split('/')[2]) 288 if not parent_category_id.isdigit(): 289 return None 290 291 return self.get_category(parent_category_id) 292 293 294 def get_categories(self, category_id: int) -> list: 295 """Получить дочерние категории из раздела 296 297 Attributes: 298 category_id (int): ID категории 299 300 Returns: 301 Список (list), состоящий из ID дочерних категорий раздела 302 """ 303 304 request = self.session.get(f"{MAIN_URL}/forums/{category_id}/page-1?_xfResponseType=json&_xfToken={self.token}").json() 305 if request['status'] == 'error': 306 return None 307 308 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 309 return [int(findall(r'\d+', category.find("a")['href'])[0]) for category in soup.find_all('div', compile('.*node--depth2 node--forum.*'))] 310 311 312 # MEMBER 313 def follow_member(self, member_id: int) -> Response: 314 """Изменить статус подписки на пользователя 315 316 Attributes: 317 member_id (int): ID пользователя 318 319 Returns: 320 Объект Response модуля requests 321 """ 322 323 if member_id == self.current_member.id: 324 raise ThisIsYouError(member_id) 325 326 return self.session.post(f"{MAIN_URL}/members/{member_id}/follow", {'_xfToken': self.token}) 327 328 329 def ignore_member(self, member_id: int) -> Response: 330 """Изменить статус игнорирования пользователя 331 332 Attributes: 333 member_id (int): ID пользователя 334 335 Returns: 336 Объект Response модуля requests 337 """ 338 339 if member_id == self.current_member.id: 340 raise ThisIsYouError(member_id) 341 342 return self.session.post(f"{MAIN_URL}/members/{member_id}/ignore", {'_xfToken': self.token}) 343 344 345 def add_profile_message(self, member_id: int, message_html: str) -> Response: 346 """Отправить сообщение на стенку пользователя 347 348 Attributes: 349 member_id (int): ID пользователя 350 message_html (str): Текст сообщения. Рекомендуется использование HTML 351 352 Returns: 353 Объект Response модуля requests 354 """ 355 356 return self.session.post(f"{MAIN_URL}/members/{member_id}/post", {'_xfToken': self.token, 'message_html': message_html}) 357 358 359 def get_profile_messages(self, member_id: int, page: int = 1) -> list | None: 360 """Возвращает ID всех сообщений со стенки пользователя на странице 361 362 Attributes: 363 member_id (int): ID пользователя 364 page (int): (необяз.) Страница для поиска. По умолчанию 1 365 366 Returns: 367 - Cписок (list) с ID всех сообщений профиля 368 - None, если пользователя не существует / закрыл профиль 369 """ 370 371 request = self.session.get(f"{MAIN_URL}/members/{member_id}/page-{page}?_xfResponseType=json&_xfToken={self.token}").json() 372 if request['status'] == 'error': 373 return None 374 375 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 376 return [int(post['id'].split('-')[2]) for post in soup.find_all('article', {'id': compile('js-profilePost-*')})] 377 378 379 # POST 380 def react_post(self, post_id: int, reaction_id: int = 1) -> Response: 381 """Поставить реакцию на сообщение 382 383 Attributes: 384 post_id (int): ID сообщения 385 reaction_id (int): (необяз.) ID реакции. По умолчанию 1 386 387 Returns: 388 Объект Response модуля requests 389 """ 390 391 return self.session.post(f'{MAIN_URL}/posts/{post_id}/react?reaction_id={reaction_id}', {'_xfToken': self.token}) 392 393 394 def edit_post(self, post_id: int, message_html: str) -> Response: 395 """Отредактировать сообщение 396 397 Attributes: 398 post_id (int): ID сообщения 399 message_html (str): Новый текст сообщения. Рекомендуется использование HTML 400 401 Returns: 402 Объект Response модуля requests 403 """ 404 405 title_of_thread_post = self.get_post(post_id).thread.title 406 return self.session.post(f"{MAIN_URL}/posts/{post_id}/edit", {"title": title_of_thread_post, "message_html": message_html, "message": message_html, "_xfToken": self.token}) 407 408 409 def delete_post(self, post_id: int, reason: str, hard_delete: bool = False) -> Response: 410 """Удалить сообщение 411 412 Attributes: 413 post_id (int): ID сообщения 414 reason (str): Причина для удаления 415 hard_delete (bool): (необяз.) Полное удаление сообщения. По умолчанию False 416 417 Returns: 418 Объект Response модуля requests 419 """ 420 421 return self.session.post(f"{MAIN_URL}/posts/{post_id}/delete", {"reason": reason, "hard_delete": int(hard_delete), "_xfToken": self.token}) 422 423 424 def bookmark_post(self, post_id: int) -> Response: 425 """Добавить сообщение в закладки 426 427 Attributes: 428 post_id (int): ID сообщения 429 430 Returns: 431 Объект Response модуля requests 432 """ 433 434 return self.session.post(f"{MAIN_URL}/posts/{post_id}/bookmark", {"_xfToken": self.token}) 435 436 437 # PROFILE POST 438 def react_profile_post(self, post_id: int, reaction_id: int = 1) -> Response: 439 """Поставить реакцию на сообщение профиля 440 441 Attributes: 442 post_id (int): ID сообщения профиля 443 reaction_id (int): (необяз.) ID реакции. По умолчанию 1 444 445 Returns: 446 Объект Response модуля requests 447 """ 448 449 return self.session.post(f'{MAIN_URL}/profile-posts/{post_id}/react?reaction_id={reaction_id}', {'_xfToken': self.token}) 450 451 452 def comment_profile_post(self, post_id: int, message_html: str) -> Response: 453 """Прокомментировать сообщение профиля 454 455 Attributes: 456 post_id (int): ID сообщения 457 message_html (str): Текст комментария. Рекомендуется использование HTML 458 459 Returns: 460 Объект Response модуля requests 461 """ 462 463 return self.session.post(f"{MAIN_URL}/profile-posts/{post_id}/add-comment", {"message_html": message_html, "_xfToken": self.token}) 464 465 466 def delete_profile_post(self, post_id: int, reason: str, hard_delete: bool = False) -> Response: 467 """Удалить сообщение профиля 468 469 Attributes: 470 post_id (int): ID сообщения профиля 471 reason (str): Причина для удаления 472 hard_delete (bool): Полное удаление сообщения. По умолчанию False (необяз.) 473 474 Returns: 475 Объект Response модуля requests 476 """ 477 478 return self.session.post(f"{MAIN_URL}/profile-posts/{post_id}/delete", {"reason": reason, "hard_delete": int(hard_delete), "_xfToken": self.token}) 479 480 481 def edit_profile_post(self, post_id: int, message_html: str) -> Response: 482 """Отредактировать сообщение профиля 483 484 Attributes: 485 post_id (int): ID сообщения 486 message_html (str): Новый текст сообщения. Рекомендуется использование HTML 487 488 Returns: 489 Объект Response модуля requests 490 """ 491 492 return self.session.post(f"{MAIN_URL}/profile-posts/{post_id}/edit", {"message_html": message_html, "message": message_html, "_xfToken": self.token}) 493 494 495 # THREAD 496 def answer_thread(self, thread_id: int, message_html: str) -> Response: 497 """Оставить сообщенме в теме 498 499 Attributes: 500 thread_id (int): ID темы 501 message_html (str): Текст сообщения. Рекомендуется использование HTML 502 503 Returns: 504 Объект Response модуля requests 505 """ 506 507 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/add-reply", {'_xfToken': self.token, 'message_html': message_html}) 508 509 510 def watch_thread(self, thread_id: int, email_subscribe: bool = False, stop: bool = False) -> Response: 511 """Изменить статус отслеживания темы 512 513 Attributes: 514 thread_id (int): ID темы 515 email_subscribe (bool): (необяз.) Отправлять ли уведомления на почту. По умолчанию False 516 stop (bool): - (необяз.) Принудительно прекратить отслеживание. По умолчанию False 517 518 Returns: 519 Объект Response модуля requests 520 """ 521 522 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/watch", {'_xfToken': self.token, 'stop': int(stop), 'email_subscribe': int(email_subscribe)}) 523 524 525 def delete_thread(self, thread_id: int, reason: str, hard_delete: bool = False) -> Response: 526 """Удалить тему 527 528 Attributes: 529 thread_id (int): ID темы 530 reason (str): Причина для удаления 531 hard_delete (bool): (необяз.) Полное удаление сообщения. По умолчанию False 532 533 Returns: 534 Объект Response модуля requests 535 """ 536 537 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/delete", {"reason": reason, "hard_delete": int(hard_delete), "_xfToken": self.token}) 538 539 540 def edit_thread(self, thread_id: int, message_html: str) -> Response: 541 """Отредактировать содержимое темы 542 543 Attributes: 544 thread_id (int): ID темы 545 message_html (str): Новое содержимое ответа. Рекомендуется использование HTML 546 547 Returns: 548 Объект Response модуля requests 549 """ 550 551 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1").content, 'lxml') 552 thread_post_id = content.find('article', {'id': compile('js-post-*')})['id'].split('js-post-') 553 return self.session.post(f"{MAIN_URL}/posts/{thread_post_id}/edit", {"message_html": message_html, "message": message_html, "_xfToken": self.token}) 554 555 556 def edit_thread_info(self, thread_id: int, title: str, prefix_id: int = None, sticky: bool = True, opened: bool = True) -> Response: 557 """Изменить статус темы, ее префикс и название 558 559 Attributes: 560 thread_id (int): ID темы 561 title (str): Новое название 562 prefix_id (int): Новый ID префикса 563 sticky (bool): Закрепить (True - закреп / False - не закреп) 564 opened (bool): Открыть/закрыть тему (True - открыть / False - закрыть) 565 566 Returns: 567 Объект Response модуля requests 568 """ 569 570 data = {"_xfToken": self.token, 'title': title} 571 572 if prefix_id is not None: data.update({'prefix_id': prefix_id}) 573 if opened: data.update({"discussion_open": 1}) 574 if sticky: data.update({"sticky": 1}) 575 576 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/edit", data) 577 578 579 def get_thread_category(self, thread_id: int) -> Category: 580 """Получить объект раздела, в котором создана тема 581 582 Attributes: 583 thread_id (int): ID темы 584 585 Returns: 586 Объект Catrgory, в котормо создана тема 587 """ 588 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1").content, 'lxml') 589 590 creator_id = content.find('a', {'class': 'username'}) 591 if creator_id is None: return None 592 593 return self.get_category(int(content.find('html')['data-container-key'].strip('node-'))) 594 595 596 def get_thread_posts(self, thread_id: int, page: int = 1) -> list: 597 """Получить все сообщения из темы на странице 598 599 Attributes: 600 thread_id (int): ID темы 601 page (int): (необяз.) Cтраница для поиска. По умолчанию 1 602 603 Returns: 604 Список (list), состоящий из ID всех сообщений на странице 605 """ 606 607 request = self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-{page}?_xfResponseType=json&_xfToken={self.token}").json() 608 if request['status'] == 'error': 609 return None 610 611 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 612 return [i['id'].strip('js-post-') for i in soup.find_all('article', {'id': compile('js-post-*')})] 613 614 615 def react_thread(self, thread_id: int, reaction_id: int = 1) -> Response: 616 """Поставить реакцию на тему 617 618 Attributes: 619 thread_id (int): ID темы 620 reaction_id (int): (необяз.) ID реакции. По умолчанию 1 621 622 Returns: 623 Объект Response модуля requests 624 """ 625 626 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1").content, 'lxml') 627 thread_post_id = content.find('article', {'id': compile('js-post-*')})['id'].strip('js-post-') 628 return self.session.post(f'{MAIN_URL}/posts/{thread_post_id}/react?reaction_id={reaction_id}', {'_xfToken': self.token}) 629 630 631 # OTHER 632 def send_form(self, form_id: int, data: dict) -> Response: 633 """Заполнить форму 634 635 Attributes: 636 form_id (int): ID формы 637 data (dict): Информация для запонения в виде словаря. Форма словаря: {'question[id вопроса]' = 'необходимая информация'} | Пример: {'question[531]' = '1'} 638 639 Returns: 640 Объект Response модуля requests 641 """ 642 643 data.update({'_xfToken': self.token}) 644 return self.session.post(f"{MAIN_URL}/form/{form_id}/submit", data)
19 def __init__(self, user_agent: str, cookie: dict, do_bypass: bool = True) -> None: 20 self.user_agent = user_agent 21 self.cookie = cookie 22 self.session = session() 23 self.session.headers = {"user-agent": user_agent} 24 self.session.cookies.update(cookie) 25 26 if do_bypass: 27 name, code = str(bypass(user_agent)).split('=') 28 self.session.cookies.set(name, code) 29 30 if BeautifulSoup(self.session.get(f"{MAIN_URL}").content, 'lxml').find('html')['data-logged-in'] == "false": 31 raise IncorrectLoginData
39 @property 40 def current_member(self) -> CurrentMember: 41 """Объект текущего пользователя""" 42 43 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/account").content, 'lxml') 44 user_id = int(content.find('span', {'class': 'avatar--xxs'})['data-user-id']) 45 member_info = self.get_member(user_id) 46 47 return CurrentMember(self, user_id, member_info.username, member_info.user_title, member_info.avatar, member_info.roles, member_info.messages_count, member_info.reactions_count, member_info.trophies_count)
Объект текущего пользователя
49 @property 50 def token(self) -> str: 51 """Токен CSRF""" 52 return BeautifulSoup(self.session.get(f"{MAIN_URL}/help/terms/").content, 'lxml').find('html')['data-csrf']
Токен CSRF
55 def get_category(self, category_id: int) -> Category: 56 """Найти раздел по ID""" 57 58 request = self.session.get(f"{MAIN_URL}/forums/{category_id}?_xfResponseType=json&_xfToken={self.token}").json() 59 if request['status'] == 'error': 60 return None 61 62 content = unescape(request['html']['content']) 63 content = BeautifulSoup(content, 'lxml') 64 65 title = unescape(request['html']['title']) 66 try: pages_count = int(content.find_all('li', {'class': 'pageNav-page'})[-1].text) 67 except IndexError: pages_count = 1 68 69 return Category(self, category_id, title, pages_count)
Найти раздел по ID
72 def get_member(self, user_id: int) -> Member: 73 """Найти пользователя по ID (возвращает либо Member, либо None (если профиль закрыт / не существует))""" 74 75 request = self.session.get(f"{MAIN_URL}/members/{user_id}?_xfResponseType=json&_xfToken={self.token}").json() 76 if request['status'] == 'error': 77 return None 78 79 content = unescape(request['html']['content']) 80 content = BeautifulSoup(content, 'lxml') 81 82 username = unescape(request['html']['title']) 83 84 roles = [] 85 for i in content.find('div', {'class': 'memberHeader-banners'}).children: 86 if i.text != '\n': roles.append(i.text) 87 88 try: user_title = content.find('span', {'class': 'userTitle'}).text 89 except AttributeError: user_title = None 90 try: avatar = MAIN_URL + content.find('a', {'class': 'avatar avatar--l'})['href'] 91 except TypeError: avatar = None 92 93 messages_count = int(content.find('a', {'href': f'/search/member?user_id={user_id}'}).text.strip().replace(',', '')) 94 reactions_count = int(content.find('dl', {'class': 'pairs pairs--rows pairs--rows--centered'}).find('dd').text.strip().replace(',', '')) 95 trophies_count = int(content.find('a', {'href': f'/members/{user_id}/trophies'}).text.strip().replace(',', '')) 96 97 return Member(self, user_id, username, user_title, avatar, roles, messages_count, reactions_count, trophies_count)
Найти пользователя по ID (возвращает либо Member, либо None (если профиль закрыт / не существует))
100 def get_thread(self, thread_id: int) -> Thread: 101 """Найти тему по ID""" 102 request = self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1?_xfResponseType=json&_xfToken={self.token}").json() 103 if request['status'] == 'error': 104 return None 105 106 if request.get('redirect') is not None: 107 return self.get_thread(request['redirect'].split(MAIN_URL, maxsplit=1)[-1].split('/')[1]) 108 109 content = BeautifulSoup(unescape(request['html']['content']), 'lxml') 110 content_h1 = BeautifulSoup(unescape(request['html']['h1']), 'lxml') 111 112 creator_id = content.find('a', {'class': 'username'}) 113 try: creator = self.get_member(int(creator_id['data-user-id'])) 114 except: creator = Member(self, int(creator_id['data-user-id']), content.find('a', {'class': 'username'}).text, None, None, None, None, None, None) 115 116 create_date = int(content.find('time')['data-time']) 117 118 try: 119 prefix = content_h1.find('span', {'class': 'label'}).text 120 title = content_h1.text.strip().replace(prefix, "").strip() 121 122 except AttributeError: 123 prefix = "" 124 title = content_h1.text 125 126 thread_content_html = content.find('div', {'class': 'bbWrapper'}) 127 thread_content = thread_content_html.text 128 129 try: pages_count = int(content.find_all('li', {'class': 'pageNav-page'})[-1].text) 130 except IndexError: pages_count = 1 131 132 is_closed = False 133 if content.find('dl', {'class': 'blockStatus'}): is_closed = True 134 thread_post_id = content.find('article', {'id': compile('js-post-*')})['id'].split('js-post-', maxsplit=1)[-1] 135 136 return Thread(self, thread_id, creator, create_date, title, prefix, thread_content, thread_content_html, pages_count, thread_post_id, is_closed)
Найти тему по ID
139 def get_post(self, post_id: int) -> Post: 140 """Найти пост по ID (Post если существует, None - удален / нет доступа)""" 141 142 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/posts/{post_id}").content, 'lxml') 143 post = content.find('article', {'id': f'js-post-{post_id}'}) 144 if post is None: 145 return None 146 147 try: creator = self.get_member(int(post.find('a', {'data-xf-init': 'member-tooltip'})['data-user-id'])) 148 except: 149 user_info = post.find('a', {'data-xf-init': 'member-tooltip'}) 150 creator = Member(self, int(user_info['data-user-id']), user_info.text, None, None, None, None, None, None) 151 152 thread = self.get_thread(int(content.find('html')['data-content-key'].split('-')[-1])) 153 create_date = int(post.find('time', {'class': 'u-dt'})['data-time']) 154 bb_content = post.find('div', {'class': 'bbWrapper'}) 155 text_content = bb_content.text 156 return Post(self, post_id, creator, thread, create_date, bb_content, text_content)
Найти пост по ID (Post если существует, None - удален / нет доступа)
159 def get_profile_post(self, post_id: int) -> ProfilePost: 160 """Найти сообщение профиля по ID""" 161 162 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/profile-posts/{post_id}").content, 'lxml') 163 post = content.find('article', {'id': f'js-profilePost-{post_id}'}) 164 if post is None: 165 return None 166 167 creator = self.get_member(int(post.find('a', {'class': 'username'})['data-user-id'])) 168 profile = self.get_member(int(content.find('span', {'class': 'username'})['data-user-id'])) 169 create_date = int(post.find('time')['data-time']) 170 bb_content = post.find('div', {'class': 'bbWrapper'}) 171 text_content = bb_content.text 172 173 return ProfilePost(self, post_id, creator, profile, create_date, bb_content, text_content)
Найти сообщение профиля по ID
175 def get_forum_statistic(self) -> Statistic: 176 """Получить статистику форума""" 177 178 content = BeautifulSoup(self.session.get(MAIN_URL).content, 'lxml') 179 threads_count = int(content.find('dl', {'class': 'pairs pairs--justified count--threads'}).find('dd').text.replace(',', '')) 180 posts_count = int(content.find('dl', {'class': 'pairs pairs--justified count--messages'}).find('dd').text.replace(',', '')) 181 users_count = int(content.find('dl', {'class': 'pairs pairs--justified count--users'}).find('dd').text.replace(',', '')) 182 last_register_member = self.get_member(int(content.find('dl', {'class': 'pairs pairs--justified'}).find('a')['data-user-id'])) 183 184 return Statistic(self, threads_count, posts_count, users_count, last_register_member)
Получить статистику форума
191 def create_thread(self, category_id: int, title: str, message_html: str, discussion_type: str = 'discussion', watch_thread: bool = True) -> Response: 192 """Создать тему в категории 193 194 Attributes: 195 category_id (int): ID категории 196 title (str): Название темы 197 message_html (str): Содержание темы. Рекомендуется использование HTML 198 discussion_type (str): (необяз.) Тип темы | Возможные варианты: 'discussion' - обсуждение (по умолчанию), 'article' - статья, 'poll' - опрос 199 watch_thread (str): (необяз.) Отслеживать ли тему. По умолчанию True 200 201 Returns: 202 Объект Response модуля requests 203 204 Todo: 205 Cделать возврат ID новой темы 206 """ 207 208 return self.session.post(f"{MAIN_URL}/forums/{category_id}/post-thread?inline-mode=1", {'_xfToken': self.token, 'title': title, 'message_html': message_html, 'discussion_type': discussion_type, 'watch_thread': int(watch_thread)})
Создать тему в категории
Attributes:
- category_id (int): ID категории
- title (str): Название темы
- message_html (str): Содержание темы. Рекомендуется использование HTML
- discussion_type (str): (необяз.) Тип темы | Возможные варианты: 'discussion' - обсуждение (по умолчанию), 'article' - статья, 'poll' - опрос
- watch_thread (str): (необяз.) Отслеживать ли тему. По умолчанию True
Returns:
Объект Response модуля requests
Todo:
Cделать возврат ID новой темы
211 def set_read_category(self, category_id: int) -> Response: 212 """Отметить категорию как прочитанную 213 214 Attributes: 215 category_id (int): ID категории 216 217 Returns: 218 Объект Response модуля requests 219 """ 220 221 return self.session.post(f"{MAIN_URL}/forums/{category_id}/mark-read", {'_xfToken': self.token})
Отметить категорию как прочитанную
Attributes:
- category_id (int): ID категории
Returns:
Объект Response модуля requests
224 def watch_category(self, category_id: int, notify: str, send_alert: bool = True, send_email: bool = False, stop: bool = False) -> Response: 225 """Настроить отслеживание категории 226 227 Attributes: 228 category_id (int): ID категории 229 notify (str): Объект отслеживания. Возможные варианты: "thread", "message", "" 230 send_alert (bool): (необяз.) Отправлять ли уведомления на форуме. По умолчанию True 231 send_email (bool): (необяз.) Отправлять ли уведомления на почту. По умолчанию False 232 stop (bool): (необяз.) Принудительное завершение отслеживания. По умолчанию False 233 234 Returns: 235 Объект Response модуля requests 236 """ 237 238 if stop: return self.session.post(f"{MAIN_URL}/forums/{category_id}/watch", {'_xfToken': self.token, 'stop': "1"}) 239 else: return self.session.post(f"{MAIN_URL}/forums/{category_id}/watch", {'_xfToken': self.token, 'send_alert': int(send_alert), 'send_email': int(send_email), 'notify': notify})
Настроить отслеживание категории
Attributes:
- category_id (int): ID категории
- notify (str): Объект отслеживания. Возможные варианты: "thread", "message", ""
- send_alert (bool): (необяз.) Отправлять ли уведомления на форуме. По умолчанию True
- send_email (bool): (необяз.) Отправлять ли уведомления на почту. По умолчанию False
- stop (bool): (необяз.) Принудительное завершение отслеживания. По умолчанию False
Returns:
Объект Response модуля requests
242 def get_category_threads(self, category_id: int, page: int = 1) -> dict: 243 """Получить темы из раздела 244 245 Attributes: 246 category_id (int): ID категории 247 page (int): (необяз.) Cтраница для поиска. По умолчанию 1 248 249 Returns: 250 Словарь (dict) по структуре THREAD_ID : 'pin'/'unpin' 251 """ 252 253 request = self.session.get(f"{MAIN_URL}/forums/{category_id}/page-{page}?_xfResponseType=json&_xfToken={self.token}").json() 254 if request['status'] == 'error': 255 return None 256 257 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 258 result = {} 259 for thread in soup.find_all('div', compile('structItem structItem--thread.*')): 260 link = thread.find_all('div', "structItem-title")[0].find_all("a")[-1] 261 262 thread_id = findall(r'\d+', link['href']) # 263 if len(thread_id) < 1: continue 264 thread_id = int(thread_id[0]) 265 266 template = {thread_id: 'unpin'} 267 if len(thread.find_all('i', {'title': 'Закреплено'})) > 0: template[thread_id] = 'pin' 268 269 result.update(template) 270 271 return result
Получить темы из раздела
Attributes:
- category_id (int): ID категории
- page (int): (необяз.) Cтраница для поиска. По умолчанию 1
Returns:
Словарь (dict) по структуре THREAD_ID : 'pin'/'unpin'
274 def get_parent_category_of_category(self, category_id: int) -> Category: 275 """Получить родительский раздел раздела 276 277 Attributes: 278 category_id (int): ID категории 279 280 Returns: 281 - Если существует: Объект Catrgory, в котором создан раздел 282 - Если не существует: None 283 """ 284 285 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/forums/{category_id}").content, 'lxml') 286 287 parent_category_id = str(content.find('ul', {'class': 'p-breadcrumbs'}).find_all('li')[-1].find('a')['href'].split('/')[2]) 288 if not parent_category_id.isdigit(): 289 return None 290 291 return self.get_category(parent_category_id)
Получить родительский раздел раздела
Attributes:
- category_id (int): ID категории
Returns:
- Если существует: Объект Catrgory, в котором создан раздел
- Если не существует: None
294 def get_categories(self, category_id: int) -> list: 295 """Получить дочерние категории из раздела 296 297 Attributes: 298 category_id (int): ID категории 299 300 Returns: 301 Список (list), состоящий из ID дочерних категорий раздела 302 """ 303 304 request = self.session.get(f"{MAIN_URL}/forums/{category_id}/page-1?_xfResponseType=json&_xfToken={self.token}").json() 305 if request['status'] == 'error': 306 return None 307 308 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 309 return [int(findall(r'\d+', category.find("a")['href'])[0]) for category in soup.find_all('div', compile('.*node--depth2 node--forum.*'))]
Получить дочерние категории из раздела
Attributes:
- category_id (int): ID категории
Returns:
Список (list), состоящий из ID дочерних категорий раздела
313 def follow_member(self, member_id: int) -> Response: 314 """Изменить статус подписки на пользователя 315 316 Attributes: 317 member_id (int): ID пользователя 318 319 Returns: 320 Объект Response модуля requests 321 """ 322 323 if member_id == self.current_member.id: 324 raise ThisIsYouError(member_id) 325 326 return self.session.post(f"{MAIN_URL}/members/{member_id}/follow", {'_xfToken': self.token})
Изменить статус подписки на пользователя
Attributes:
- member_id (int): ID пользователя
Returns:
Объект Response модуля requests
329 def ignore_member(self, member_id: int) -> Response: 330 """Изменить статус игнорирования пользователя 331 332 Attributes: 333 member_id (int): ID пользователя 334 335 Returns: 336 Объект Response модуля requests 337 """ 338 339 if member_id == self.current_member.id: 340 raise ThisIsYouError(member_id) 341 342 return self.session.post(f"{MAIN_URL}/members/{member_id}/ignore", {'_xfToken': self.token})
Изменить статус игнорирования пользователя
Attributes:
- member_id (int): ID пользователя
Returns:
Объект Response модуля requests
345 def add_profile_message(self, member_id: int, message_html: str) -> Response: 346 """Отправить сообщение на стенку пользователя 347 348 Attributes: 349 member_id (int): ID пользователя 350 message_html (str): Текст сообщения. Рекомендуется использование HTML 351 352 Returns: 353 Объект Response модуля requests 354 """ 355 356 return self.session.post(f"{MAIN_URL}/members/{member_id}/post", {'_xfToken': self.token, 'message_html': message_html})
Отправить сообщение на стенку пользователя
Attributes:
- member_id (int): ID пользователя
- message_html (str): Текст сообщения. Рекомендуется использование HTML
Returns:
Объект Response модуля requests
359 def get_profile_messages(self, member_id: int, page: int = 1) -> list | None: 360 """Возвращает ID всех сообщений со стенки пользователя на странице 361 362 Attributes: 363 member_id (int): ID пользователя 364 page (int): (необяз.) Страница для поиска. По умолчанию 1 365 366 Returns: 367 - Cписок (list) с ID всех сообщений профиля 368 - None, если пользователя не существует / закрыл профиль 369 """ 370 371 request = self.session.get(f"{MAIN_URL}/members/{member_id}/page-{page}?_xfResponseType=json&_xfToken={self.token}").json() 372 if request['status'] == 'error': 373 return None 374 375 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 376 return [int(post['id'].split('-')[2]) for post in soup.find_all('article', {'id': compile('js-profilePost-*')})]
Возвращает ID всех сообщений со стенки пользователя на странице
Attributes:
- member_id (int): ID пользователя
- page (int): (необяз.) Страница для поиска. По умолчанию 1
Returns:
- Cписок (list) с ID всех сообщений профиля
- None, если пользователя не существует / закрыл профиль
380 def react_post(self, post_id: int, reaction_id: int = 1) -> Response: 381 """Поставить реакцию на сообщение 382 383 Attributes: 384 post_id (int): ID сообщения 385 reaction_id (int): (необяз.) ID реакции. По умолчанию 1 386 387 Returns: 388 Объект Response модуля requests 389 """ 390 391 return self.session.post(f'{MAIN_URL}/posts/{post_id}/react?reaction_id={reaction_id}', {'_xfToken': self.token})
Поставить реакцию на сообщение
Attributes:
- post_id (int): ID сообщения
- reaction_id (int): (необяз.) ID реакции. По умолчанию 1
Returns:
Объект Response модуля requests
394 def edit_post(self, post_id: int, message_html: str) -> Response: 395 """Отредактировать сообщение 396 397 Attributes: 398 post_id (int): ID сообщения 399 message_html (str): Новый текст сообщения. Рекомендуется использование HTML 400 401 Returns: 402 Объект Response модуля requests 403 """ 404 405 title_of_thread_post = self.get_post(post_id).thread.title 406 return self.session.post(f"{MAIN_URL}/posts/{post_id}/edit", {"title": title_of_thread_post, "message_html": message_html, "message": message_html, "_xfToken": self.token})
Отредактировать сообщение
Attributes:
- post_id (int): ID сообщения
- message_html (str): Новый текст сообщения. Рекомендуется использование HTML
Returns:
Объект Response модуля requests
409 def delete_post(self, post_id: int, reason: str, hard_delete: bool = False) -> Response: 410 """Удалить сообщение 411 412 Attributes: 413 post_id (int): ID сообщения 414 reason (str): Причина для удаления 415 hard_delete (bool): (необяз.) Полное удаление сообщения. По умолчанию False 416 417 Returns: 418 Объект Response модуля requests 419 """ 420 421 return self.session.post(f"{MAIN_URL}/posts/{post_id}/delete", {"reason": reason, "hard_delete": int(hard_delete), "_xfToken": self.token})
Удалить сообщение
Attributes:
- post_id (int): ID сообщения
- reason (str): Причина для удаления
- hard_delete (bool): (необяз.) Полное удаление сообщения. По умолчанию False
Returns:
Объект Response модуля requests
424 def bookmark_post(self, post_id: int) -> Response: 425 """Добавить сообщение в закладки 426 427 Attributes: 428 post_id (int): ID сообщения 429 430 Returns: 431 Объект Response модуля requests 432 """ 433 434 return self.session.post(f"{MAIN_URL}/posts/{post_id}/bookmark", {"_xfToken": self.token})
Добавить сообщение в закладки
Attributes:
- post_id (int): ID сообщения
Returns:
Объект Response модуля requests
438 def react_profile_post(self, post_id: int, reaction_id: int = 1) -> Response: 439 """Поставить реакцию на сообщение профиля 440 441 Attributes: 442 post_id (int): ID сообщения профиля 443 reaction_id (int): (необяз.) ID реакции. По умолчанию 1 444 445 Returns: 446 Объект Response модуля requests 447 """ 448 449 return self.session.post(f'{MAIN_URL}/profile-posts/{post_id}/react?reaction_id={reaction_id}', {'_xfToken': self.token})
Поставить реакцию на сообщение профиля
Attributes:
- post_id (int): ID сообщения профиля
- reaction_id (int): (необяз.) ID реакции. По умолчанию 1
Returns:
Объект Response модуля requests
452 def comment_profile_post(self, post_id: int, message_html: str) -> Response: 453 """Прокомментировать сообщение профиля 454 455 Attributes: 456 post_id (int): ID сообщения 457 message_html (str): Текст комментария. Рекомендуется использование HTML 458 459 Returns: 460 Объект Response модуля requests 461 """ 462 463 return self.session.post(f"{MAIN_URL}/profile-posts/{post_id}/add-comment", {"message_html": message_html, "_xfToken": self.token})
Прокомментировать сообщение профиля
Attributes:
- post_id (int): ID сообщения
- message_html (str): Текст комментария. Рекомендуется использование HTML
Returns:
Объект Response модуля requests
466 def delete_profile_post(self, post_id: int, reason: str, hard_delete: bool = False) -> Response: 467 """Удалить сообщение профиля 468 469 Attributes: 470 post_id (int): ID сообщения профиля 471 reason (str): Причина для удаления 472 hard_delete (bool): Полное удаление сообщения. По умолчанию False (необяз.) 473 474 Returns: 475 Объект Response модуля requests 476 """ 477 478 return self.session.post(f"{MAIN_URL}/profile-posts/{post_id}/delete", {"reason": reason, "hard_delete": int(hard_delete), "_xfToken": self.token})
Удалить сообщение профиля
Attributes:
- post_id (int): ID сообщения профиля
- reason (str): Причина для удаления
- hard_delete (bool): Полное удаление сообщения. По умолчанию False (необяз.)
Returns:
Объект Response модуля requests
481 def edit_profile_post(self, post_id: int, message_html: str) -> Response: 482 """Отредактировать сообщение профиля 483 484 Attributes: 485 post_id (int): ID сообщения 486 message_html (str): Новый текст сообщения. Рекомендуется использование HTML 487 488 Returns: 489 Объект Response модуля requests 490 """ 491 492 return self.session.post(f"{MAIN_URL}/profile-posts/{post_id}/edit", {"message_html": message_html, "message": message_html, "_xfToken": self.token})
Отредактировать сообщение профиля
Attributes:
- post_id (int): ID сообщения
- message_html (str): Новый текст сообщения. Рекомендуется использование HTML
Returns:
Объект Response модуля requests
496 def answer_thread(self, thread_id: int, message_html: str) -> Response: 497 """Оставить сообщенме в теме 498 499 Attributes: 500 thread_id (int): ID темы 501 message_html (str): Текст сообщения. Рекомендуется использование HTML 502 503 Returns: 504 Объект Response модуля requests 505 """ 506 507 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/add-reply", {'_xfToken': self.token, 'message_html': message_html})
Оставить сообщенме в теме
Attributes:
- thread_id (int): ID темы
- message_html (str): Текст сообщения. Рекомендуется использование HTML
Returns:
Объект Response модуля requests
510 def watch_thread(self, thread_id: int, email_subscribe: bool = False, stop: bool = False) -> Response: 511 """Изменить статус отслеживания темы 512 513 Attributes: 514 thread_id (int): ID темы 515 email_subscribe (bool): (необяз.) Отправлять ли уведомления на почту. По умолчанию False 516 stop (bool): - (необяз.) Принудительно прекратить отслеживание. По умолчанию False 517 518 Returns: 519 Объект Response модуля requests 520 """ 521 522 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/watch", {'_xfToken': self.token, 'stop': int(stop), 'email_subscribe': int(email_subscribe)})
Изменить статус отслеживания темы
Attributes:
- thread_id (int): ID темы
- email_subscribe (bool): (необяз.) Отправлять ли уведомления на почту. По умолчанию False
- stop (bool): - (необяз.) Принудительно прекратить отслеживание. По умолчанию False
Returns:
Объект Response модуля requests
525 def delete_thread(self, thread_id: int, reason: str, hard_delete: bool = False) -> Response: 526 """Удалить тему 527 528 Attributes: 529 thread_id (int): ID темы 530 reason (str): Причина для удаления 531 hard_delete (bool): (необяз.) Полное удаление сообщения. По умолчанию False 532 533 Returns: 534 Объект Response модуля requests 535 """ 536 537 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/delete", {"reason": reason, "hard_delete": int(hard_delete), "_xfToken": self.token})
Удалить тему
Attributes:
- thread_id (int): ID темы
- reason (str): Причина для удаления
- hard_delete (bool): (необяз.) Полное удаление сообщения. По умолчанию False
Returns:
Объект Response модуля requests
540 def edit_thread(self, thread_id: int, message_html: str) -> Response: 541 """Отредактировать содержимое темы 542 543 Attributes: 544 thread_id (int): ID темы 545 message_html (str): Новое содержимое ответа. Рекомендуется использование HTML 546 547 Returns: 548 Объект Response модуля requests 549 """ 550 551 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1").content, 'lxml') 552 thread_post_id = content.find('article', {'id': compile('js-post-*')})['id'].split('js-post-') 553 return self.session.post(f"{MAIN_URL}/posts/{thread_post_id}/edit", {"message_html": message_html, "message": message_html, "_xfToken": self.token})
Отредактировать содержимое темы
Attributes:
- thread_id (int): ID темы
- message_html (str): Новое содержимое ответа. Рекомендуется использование HTML
Returns:
Объект Response модуля requests
556 def edit_thread_info(self, thread_id: int, title: str, prefix_id: int = None, sticky: bool = True, opened: bool = True) -> Response: 557 """Изменить статус темы, ее префикс и название 558 559 Attributes: 560 thread_id (int): ID темы 561 title (str): Новое название 562 prefix_id (int): Новый ID префикса 563 sticky (bool): Закрепить (True - закреп / False - не закреп) 564 opened (bool): Открыть/закрыть тему (True - открыть / False - закрыть) 565 566 Returns: 567 Объект Response модуля requests 568 """ 569 570 data = {"_xfToken": self.token, 'title': title} 571 572 if prefix_id is not None: data.update({'prefix_id': prefix_id}) 573 if opened: data.update({"discussion_open": 1}) 574 if sticky: data.update({"sticky": 1}) 575 576 return self.session.post(f"{MAIN_URL}/threads/{thread_id}/edit", data)
Изменить статус темы, ее префикс и название
Attributes:
- thread_id (int): ID темы
- title (str): Новое название
- prefix_id (int): Новый ID префикса
- sticky (bool): Закрепить (True - закреп / False - не закреп)
- opened (bool): Открыть/закрыть тему (True - открыть / False - закрыть)
Returns:
Объект Response модуля requests
579 def get_thread_category(self, thread_id: int) -> Category: 580 """Получить объект раздела, в котором создана тема 581 582 Attributes: 583 thread_id (int): ID темы 584 585 Returns: 586 Объект Catrgory, в котормо создана тема 587 """ 588 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1").content, 'lxml') 589 590 creator_id = content.find('a', {'class': 'username'}) 591 if creator_id is None: return None 592 593 return self.get_category(int(content.find('html')['data-container-key'].strip('node-')))
Получить объект раздела, в котором создана тема
Attributes:
- thread_id (int): ID темы
Returns:
Объект Catrgory, в котормо создана тема
596 def get_thread_posts(self, thread_id: int, page: int = 1) -> list: 597 """Получить все сообщения из темы на странице 598 599 Attributes: 600 thread_id (int): ID темы 601 page (int): (необяз.) Cтраница для поиска. По умолчанию 1 602 603 Returns: 604 Список (list), состоящий из ID всех сообщений на странице 605 """ 606 607 request = self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-{page}?_xfResponseType=json&_xfToken={self.token}").json() 608 if request['status'] == 'error': 609 return None 610 611 soup = BeautifulSoup(unescape(request['html']['content']), "lxml") 612 return [i['id'].strip('js-post-') for i in soup.find_all('article', {'id': compile('js-post-*')})]
Получить все сообщения из темы на странице
Attributes:
- thread_id (int): ID темы
- page (int): (необяз.) Cтраница для поиска. По умолчанию 1
Returns:
Список (list), состоящий из ID всех сообщений на странице
615 def react_thread(self, thread_id: int, reaction_id: int = 1) -> Response: 616 """Поставить реакцию на тему 617 618 Attributes: 619 thread_id (int): ID темы 620 reaction_id (int): (необяз.) ID реакции. По умолчанию 1 621 622 Returns: 623 Объект Response модуля requests 624 """ 625 626 content = BeautifulSoup(self.session.get(f"{MAIN_URL}/threads/{thread_id}/page-1").content, 'lxml') 627 thread_post_id = content.find('article', {'id': compile('js-post-*')})['id'].strip('js-post-') 628 return self.session.post(f'{MAIN_URL}/posts/{thread_post_id}/react?reaction_id={reaction_id}', {'_xfToken': self.token})
Поставить реакцию на тему
Attributes:
- thread_id (int): ID темы
- reaction_id (int): (необяз.) ID реакции. По умолчанию 1
Returns:
Объект Response модуля requests
632 def send_form(self, form_id: int, data: dict) -> Response: 633 """Заполнить форму 634 635 Attributes: 636 form_id (int): ID формы 637 data (dict): Информация для запонения в виде словаря. Форма словаря: {'question[id вопроса]' = 'необходимая информация'} | Пример: {'question[531]' = '1'} 638 639 Returns: 640 Объект Response модуля requests 641 """ 642 643 data.update({'_xfToken': self.token}) 644 return self.session.post(f"{MAIN_URL}/form/{form_id}/submit", data)
Заполнить форму
Attributes:
- form_id (int): ID формы
- data (dict): Информация для запонения в виде словаря. Форма словаря: {'question[id вопроса]' = 'необходимая информация'} | Пример: {'question[531]' = '1'}
Returns:
Объект Response модуля requests