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)
class ArizonaAPI:
 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)
ArizonaAPI(user_agent: str, cookie: dict, do_bypass: bool = True)
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
user_agent
cookie
session
def logout(self):
34    def logout(self):
35        """Закрыть сессию"""
36        return self.session.close()

Закрыть сессию

current_member: arz_api.models.member_object.CurrentMember
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)

Объект текущего пользователя

token: str
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

def get_category(self, category_id: int) -> arz_api.models.category_object.Category:
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

def get_member(self, user_id: int) -> arz_api.models.member_object.Member:
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 (если профиль закрыт / не существует))

def get_thread(self, thread_id: int) -> arz_api.models.thread_object.Thread:
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

def get_post(self, post_id: int) -> arz_api.models.post_object.Post:
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 - удален / нет доступа)

def get_profile_post(self, post_id: int) -> arz_api.models.post_object.ProfilePost:
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

def get_forum_statistic(self) -> arz_api.models.other.Statistic:
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)

Получить статистику форума

def create_thread( self, category_id: int, title: str, message_html: str, discussion_type: str = 'discussion', watch_thread: bool = True) -> requests.models.Response:
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 новой темы

def set_read_category(self, category_id: int) -> requests.models.Response:
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

def watch_category( self, category_id: int, notify: str, send_alert: bool = True, send_email: bool = False, stop: bool = False) -> requests.models.Response:
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

def get_category_threads(self, category_id: int, page: int = 1) -> dict:
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'

def get_parent_category_of_category(self, category_id: int) -> arz_api.models.category_object.Category:
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
def get_categories(self, category_id: int) -> list:
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 дочерних категорий раздела

def follow_member(self, member_id: int) -> requests.models.Response:
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

def ignore_member(self, member_id: int) -> requests.models.Response:
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

def add_profile_message(self, member_id: int, message_html: str) -> requests.models.Response:
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

def get_profile_messages(self, member_id: int, page: int = 1) -> list | None:
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, если пользователя не существует / закрыл профиль
def react_post(self, post_id: int, reaction_id: int = 1) -> requests.models.Response:
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

def edit_post(self, post_id: int, message_html: str) -> requests.models.Response:
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

def delete_post( self, post_id: int, reason: str, hard_delete: bool = False) -> requests.models.Response:
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

def bookmark_post(self, post_id: int) -> requests.models.Response:
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

def react_profile_post(self, post_id: int, reaction_id: int = 1) -> requests.models.Response:
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

def comment_profile_post(self, post_id: int, message_html: str) -> requests.models.Response:
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

def delete_profile_post( self, post_id: int, reason: str, hard_delete: bool = False) -> requests.models.Response:
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

def edit_profile_post(self, post_id: int, message_html: str) -> requests.models.Response:
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

def answer_thread(self, thread_id: int, message_html: str) -> requests.models.Response:
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

def watch_thread( self, thread_id: int, email_subscribe: bool = False, stop: bool = False) -> requests.models.Response:
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

def delete_thread( self, thread_id: int, reason: str, hard_delete: bool = False) -> requests.models.Response:
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

def edit_thread(self, thread_id: int, message_html: str) -> requests.models.Response:
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

def edit_thread_info( self, thread_id: int, title: str, prefix_id: int = None, sticky: bool = True, opened: bool = True) -> requests.models.Response:
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

def get_thread_category(self, thread_id: int) -> arz_api.models.category_object.Category:
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, в котормо создана тема

def get_thread_posts(self, thread_id: int, page: int = 1) -> list:
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 всех сообщений на странице

def react_thread(self, thread_id: int, reaction_id: int = 1) -> requests.models.Response:
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

def send_form(self, form_id: int, data: dict) -> requests.models.Response:
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