Guest User

koneAPI

a guest
May 17th, 2025
94
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 20.24 KB | Source Code | 0 0
  1. import requests
  2. import json
  3. import os
  4. from bs4 import BeautifulSoup
  5. from typing import Dict, Any, Optional, List
  6. import re
  7.  
  8. class KoneBaseClient:
  9.     """Kone API의 기본 클라이언트"""
  10.  
  11.     def __init__(self, secure_neko_cookie: str):
  12.         """
  13.        Args:
  14.            secure_neko_cookie: __Secure_Neko 쿠키 값
  15.        """
  16.         self.headers = {
  17.             'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0'
  18.         }
  19.         self.cookies = {
  20.             '__Secure_Neko': secure_neko_cookie
  21.         }
  22.  
  23.     def _request(self, method: str, url: str, **kwargs) -> requests.Response:
  24.         """API 요청을 보내는 기본 메서드
  25.  
  26.        Args:
  27.            method: HTTP 메서드 (GET, POST, OPTIONS 등)
  28.            url: 요청 URL
  29.            **kwargs: requests 라이브러리에 전달할 추가 인수
  30.  
  31.        Returns:
  32.            Response 객체
  33.        """
  34.         # 헤더와 쿠키 설정
  35.         if 'headers' not in kwargs:
  36.             kwargs['headers'] = {}
  37.         if 'cookies' not in kwargs:
  38.             kwargs['cookies'] = {}
  39.  
  40.         # 기본 헤더와 쿠키 병합
  41.         kwargs['headers'].update(self.headers)
  42.         kwargs['cookies'].update(self.cookies)
  43.  
  44.         # 요청 실행
  45.         response = requests.request(method, url, **kwargs)
  46.         return response
  47.  
  48.  
  49. class KoneAPI(KoneBaseClient):
  50.     """API 서버 (api.kone.gg)에 대한 클라이언트
  51.    참고: 모든 이미지는 WEBP 포맷입니다.
  52.    """
  53.  
  54.     def __init__(self, secure_neko_cookie: str, sub_name: str = "trickcalrevive"):
  55.         """
  56.        Args:
  57.            secure_neko_cookie: __Secure_Neko 쿠키 값
  58.            sub_name: 서브 이름 (기본값: trickcalrevive)
  59.        """
  60.         super().__init__(secure_neko_cookie)
  61.         self.base_url = "https://api.kone.gg"
  62.         self.sub_name = sub_name
  63.  
  64.     # 배너 관련 API
  65.     def get_sub_banner(self) -> requests.Response:
  66.         """서브 배너 가져오기 (WEBP 포맷)"""
  67.         url = f"{self.base_url}/v0/sub/{self.sub_name}/banner"
  68.         return self._request("GET", url)
  69.  
  70.     # 아이콘 관련 API
  71.     def get_sub_icon(self) -> requests.Response:
  72.         """서브 아이콘 가져오기 (WEBP 포맷)"""
  73.         url = f"{self.base_url}/v0/sub/{self.sub_name}/icon"
  74.         return self._request("GET", url)
  75.  
  76.     def get_profile_icon(self, profile_id: str) -> requests.Response:
  77.         """프로필 아이콘 가져오기 (WEBP 포맷)
  78.  
  79.        Args:
  80.            profile_id: 프로필 ID
  81.        """
  82.         url = f"{self.base_url}/v0/profile/{profile_id}/icon"
  83.         return self._request("GET", url)
  84.  
  85.     # 운영자 관련 API
  86.     def get_sub_moderators(self) -> requests.Response:
  87.         """서브 운영자 목록 가져오기"""
  88.         url = f"{self.base_url}/v0/sub/{self.sub_name}/moderators"
  89.         return self._request("GET", url)
  90.  
  91.     def options_sub_moderators(self) -> requests.Response:
  92.         """서브 운영자 OPTIONS 요청"""
  93.         url = f"{self.base_url}/v0/sub/{self.sub_name}/moderators"
  94.         return self._request("OPTIONS", url)
  95.  
  96.     # 공지사항 관련 API
  97.     def get_sub_notices(self) -> requests.Response:
  98.         """서브 공지사항 목록 가져오기"""
  99.         url = f"{self.base_url}/v0/sub/{self.sub_name}/notices"
  100.         return self._request("GET", url)
  101.  
  102.     def options_sub_notices(self) -> requests.Response:
  103.         """서브 공지사항 OPTIONS 요청"""
  104.         url = f"{self.base_url}/v0/sub/{self.sub_name}/notices"
  105.         return self._request("OPTIONS", url)
  106.  
  107.     # 게시글 관련 API
  108.     def post_sub_article(self, title: str, content: str, text_type: str = "text") -> requests.Response:
  109.         """서브 게시글 작성
  110.  
  111.        Args:
  112.            title: 게시글 제목
  113.            content: 게시글 내용 (HTML 형식)
  114.            text_type: 게시글 카테고리 (선택 사항, 기본값: text)
  115.        """
  116.         url = f"{self.base_url}/v0/article/publish/{self.sub_name}"
  117.         data = {
  118.             "title": title,
  119.             "content": content,
  120.             "type": text_type
  121.         }
  122.         return self._request("POST", url, json=data)
  123.  
  124.     def edit_sub_article(self, article_id: str,title: str, content: str) -> requests.Response:
  125.         """서브 게시글 수정
  126.  
  127.        Args:
  128.            article_id: 수정할 게시글 제목
  129.            title: 게시글 제목
  130.            content: 게시글 내용 (HTML 형식)
  131.        """
  132.         url = f"{self.base_url}/v0/article/{article_id}"
  133.         data = {
  134.             "title": title,
  135.             "content": content,
  136.             "category": None
  137.         }
  138.         return self._request("PATCH", url, json=data)
  139.  
  140.     def options_sub_article(self) -> requests.Response:
  141.         """서브 게시글 OPTIONS 요청"""
  142.         url = f"{self.base_url}/v0/article/publish/{self.sub_name}"
  143.         return self._request("OPTIONS", url)
  144.  
  145.     # 댓글 관련 API
  146.     def post_comment(self, article_id: str, content: str, comment_type: str = "text") -> requests.Response:
  147.         """댓글 작성
  148.  
  149.        Args:
  150.            article_id: 게시글 ID
  151.            content: 댓글 내용
  152.            comment_type: 댓글 유형 (기본값: text)
  153.        """
  154.         url = f"{self.base_url}/v0/comment/write"
  155.         data = {
  156.             "article_id": article_id,
  157.             "content": content,
  158.             "type": comment_type
  159.         }
  160.         return self._request("POST", url, json=data)
  161.  
  162.     def edit_comment(self, comment_id: str, content: str) -> requests.Response:
  163.         """댓글 수정
  164.  
  165.        Args:
  166.            comment_id: 댓글 ID
  167.            content: 수정할 댓글 내용
  168.        """
  169.         url = f"{self.base_url}/v0/comment/{comment_id}"
  170.         data = {
  171.             "content": content
  172.         }
  173.         return self._request("PATCH", url, json=data)
  174.  
  175.     def options_comment(self) -> requests.Response:
  176.         """댓글 OPTIONS 요청"""
  177.         url = f"{self.base_url}/v0/comment/write"
  178.         return self._request("OPTIONS", url)
  179.  
  180.     def get_comment(self, comment_id: str) -> requests.Response:
  181.         """댓글 정보 가져오기
  182.  
  183.        Args:
  184.            comment_id: 댓글 ID
  185.        """
  186.         url = f"{self.base_url}/v0/comment/{comment_id}"
  187.         return self._request("GET", url)
  188.  
  189.     def options_comment_by_id(self, comment_id: str) -> requests.Response:
  190.         """특정 댓글 OPTIONS 요청
  191.  
  192.        Args:
  193.            comment_id: 댓글 ID
  194.        """
  195.         url = f"{self.base_url}/v0/comment/{comment_id}"
  196.         return self._request("OPTIONS", url)
  197.  
  198.  
  199. class KoneWebAPI(KoneBaseClient):
  200.     """웹 서버 (kone.gg)에 대한 클라이언트
  201.    참고: web 서브 웹 페이지는 모두 HTML 포맷입니다.
  202.    """
  203.  
  204.     def __init__(self, secure_neko_cookie: str, sub_name: str = "trickcalrevive"):
  205.         """
  206.        Args:
  207.            secure_neko_cookie: __Secure_Neko 쿠키 값
  208.            sub_name: 서브 이름 (기본값: trickcalrevive)
  209.        """
  210.         super().__init__(secure_neko_cookie)
  211.         self.base_url = "https://kone.gg"
  212.         self.sub_name = sub_name
  213.  
  214.     def get_sub_list(self, rsc: Optional[str] = None) -> requests.Response:
  215.         """서브 목록 가져오기
  216.  
  217.        Args:
  218.            rsc: _rsc 쿼리 매개변수 (선택 사항)
  219.        """
  220.         url = f"{self.base_url}/sub-list"
  221.         params = {}
  222.         if rsc:
  223.             params["_rsc"] = rsc
  224.         return self._request("GET", url, params=params)
  225.  
  226.     def get_sub(self, rsc: Optional[str] = None) -> requests.Response:
  227.         """서브 메인 페이지(HTML) 가져오기
  228.  
  229.        Args:
  230.            rsc: _rsc 쿼리 매개변수 (선택 사항)
  231.        """
  232.         url = f"{self.base_url}/s/{self.sub_name}"
  233.         params = {}
  234.         if rsc:
  235.             params["_rsc"] = rsc
  236.         return self._request("GET", url, params=params)
  237.  
  238.     def get_sub_posts(self, rsc: Optional[str] = None) -> List[Dict[str, Any]]:
  239.         """서브 메인 페이지에서 게시글 목록을 파싱하여 반환(DICT)
  240.  
  241.        Args:
  242.            rsc: _rsc 쿼리 매개변수 (선택 사항)
  243.  
  244.        Returns:
  245.            파싱된 게시글 정보 목록 (딕셔너리 리스트)
  246.        """
  247.         # 서브 메인 페이지 가져오기
  248.         response = self.get_sub(rsc)
  249.  
  250.         if response.status_code != 200:
  251.             raise Exception(f"서브 페이지 가져오기 실패: {response.status_code}")
  252.  
  253.         # HTML 파싱
  254.         soup = BeautifulSoup(response.text, 'html.parser')
  255.         posts = []
  256.  
  257.         # 게시글 링크 요소 찾기
  258.         article_links = soup.select('a.group.overflow-hidden')
  259.  
  260.         for link in article_links:
  261.             post = {}
  262.  
  263.             # 게시글 ID와 URL 추출
  264.             href = link.get('href', '')
  265.             if href:
  266.                 post['article_id'] = href.split('/')[-1].split('#')[0]
  267.                 post['url'] = href
  268.  
  269.             # 제목 추출
  270.             title_span = link.select_one('span.overflow-hidden.text-nowrap.text-ellipsis')
  271.             if title_span:
  272.                 post['title'] = title_span.text.strip()
  273.  
  274.             # PC 버전과 모바일 버전 레이아웃 처리
  275.             # PC 버전 파싱 시도
  276.             md_contents = link.select_one('.hidden.md\\:contents')
  277.             if md_contents:
  278.                 # 작성자
  279.                 author_div = md_contents.select_one('.col-span-2.overflow-hidden')
  280.                 if author_div:
  281.                     post['author'] = author_div.text.strip()
  282.  
  283.                 # 작성일
  284.                 date_div = md_contents.select_one('.col-span-2.text-center.text-zinc-700')
  285.                 if date_div:
  286.                     post['date'] = date_div.text.strip()
  287.  
  288.                 # 조회수
  289.                 views_div = md_contents.select_one('.col-span-1.text-center.text-zinc-700')
  290.                 if views_div:
  291.                     try:
  292.                         post['views'] = int(views_div.text.strip())
  293.                     except ValueError:
  294.                         post['views'] = 0
  295.  
  296.                 # 추천수
  297.                 likes_elements = md_contents.select('.col-span-1.text-center')
  298.                 if len(likes_elements) > 1:  # 일반적으로 마지막 요소가 추천수
  299.                     likes_div = likes_elements[-1]
  300.                     try:
  301.                         post['likes'] = int(likes_div.text.strip())
  302.                     except ValueError:
  303.                         post['likes'] = 0
  304.             else:
  305.                 # 모바일 버전 파싱
  306.                 mobile_contents = link.select_one('.contents.md\\:hidden')
  307.                 if mobile_contents:
  308.                     # 작성자
  309.                     author_div = mobile_contents.select_one('.flex.items-center.justify-center.gap-1.overflow-hidden')
  310.                     if author_div:
  311.                         post['author'] = author_div.text.strip()
  312.  
  313.                     # 아이콘으로 구분된 값들 (작성일, 조회수, 추천수)
  314.                     stat_elements = mobile_contents.select('.flex.gap-1')
  315.  
  316.                     if len(stat_elements) >= 3:
  317.                         # 작성일
  318.                         post['date'] = stat_elements[0].text.strip()
  319.  
  320.                         # 조회수
  321.                         views_text = stat_elements[1].text.strip()
  322.                         try:
  323.                             post['views'] = int(views_text)
  324.                         except ValueError:
  325.                             post['views'] = 0
  326.  
  327.                         # 추천수
  328.                         likes_text = stat_elements[2].text.strip()
  329.                         try:
  330.                             post['likes'] = int(likes_text)
  331.                         except ValueError:
  332.                             post['likes'] = 0
  333.  
  334.             posts.append(post)
  335.  
  336.         return posts
  337.  
  338.     def get_sub_write(self, rsc: Optional[str] = None) -> requests.Response:
  339.         """서브 글쓰기 페이지 가져오기
  340.  
  341.        Args:
  342.            rsc: _rsc 쿼리 매개변수 (선택 사항)
  343.        """
  344.         url = f"{self.base_url}/s/{self.sub_name}/write"
  345.         params = {}
  346.         if rsc:
  347.             params["_rsc"] = rsc
  348.         return self._request("GET", url, params=params)
  349.  
  350.     def get_article(self, article_id: str, rsc: Optional[str] = None) -> requests.Response:
  351.         """특정 게시글 페이지(HTML) 가져오기
  352.  
  353.        Args:
  354.            article_id: 게시글 ID
  355.            rsc: _rsc 쿼리 매개변수 (선택 사항)
  356.        """
  357.         url = f"{self.base_url}/s/{self.sub_name}/{article_id}"
  358.         params = {}
  359.         if rsc:
  360.             params["_rsc"] = rsc
  361.         return self._request("GET", url, params=params)
  362.  
  363.     def _parse_comments_html(self, html_content):
  364.         """
  365.        게시글 HTML 내용에서 댓글을 파싱하여 리스트 형태로 반환합니다.
  366.  
  367.        Args:
  368.            html_content: 게시글 HTML 내용
  369.  
  370.        Returns:
  371.            list: 댓글 정보 리스트 (댓글 ID, 작성자, 내용, 작성 시간, 추천 수 등)
  372.        """
  373.         import re
  374.         soup = BeautifulSoup(html_content, 'html.parser')
  375.         comments = []
  376.  
  377.         # 댓글 컨테이너 찾기
  378.         comment_containers = soup.select('div.relative.px-4.py-2')
  379.  
  380.         for container in comment_containers:
  381.             # 댓글 ID 추출
  382.             comment_id_span = container.select_one('span[id^="c_"]')
  383.             if not comment_id_span:
  384.                 continue
  385.  
  386.             comment_id = comment_id_span.get('id').replace('c_', '')
  387.  
  388.             # 댓글 작성자 정보
  389.             author_link = container.select_one('a.flex.items-center.gap-1.text-sm.font-medium.hover\\:underline')
  390.             author = author_link.text.strip() if author_link else None
  391.  
  392.             author_handle = None
  393.             if author_link and author_link.get('href'):
  394.                 author_handle = author_link.get('href').replace('/u/', '')
  395.  
  396.             # 댓글 작성 시간
  397.             time_span = container.select_one('div.flex.gap-1.text-xs.text-zinc-500 span')
  398.             created_at = time_span.text.strip() if time_span else None
  399.  
  400.             # 편집 시간 (있는 경우)
  401.             edited_at = None
  402.             time_spans = container.select('div.flex.gap-1.text-xs.text-zinc-500 span')
  403.             if len(time_spans) > 1 and '(' in time_spans[1].text:
  404.                 edited_at = time_spans[1].text.strip().replace('(', '').replace(')', '')
  405.  
  406.             # 댓글 내용
  407.             content_p = container.select_one('p.text-sm.max-w-xl.whitespace-pre-wrap')
  408.             content = content_p.text.strip() if content_p else None
  409.  
  410.             # 삭제된 댓글 확인
  411.             is_deleted = False
  412.             deleted_p = container.select_one('p.text-sm.opacity-50')
  413.             if deleted_p and "삭제된 댓글입니다" in deleted_p.text:
  414.                 is_deleted = True
  415.                 content = None
  416.  
  417.             # 추천 수 (투표수)
  418.             votes = 0
  419.             # 댓글 투표수는 HTML에서 명시적으로 표시되지 않아 버튼 요소를 찾아야 함
  420.  
  421.             # 부모 댓글 ID
  422.             parent_id = None
  423.  
  424.             # 중첩된 댓글인지 확인
  425.             if 'ml-4' in container.get('class', []):
  426.                 # 부모 댓글 찾기
  427.                 parent_container = container.find_previous_sibling('div', class_='relative')
  428.                 if parent_container:
  429.                     parent_id_span = parent_container.select_one('span[id^="c_"]')
  430.                     if parent_id_span:
  431.                         parent_id = parent_id_span.get('id').replace('c_', '')
  432.  
  433.             comment = {
  434.                 'id': comment_id,
  435.                 'author': author,
  436.                 'author_handle': author_handle,
  437.                 'content': content,
  438.                 'created_at': created_at,
  439.                 'edited_at': edited_at,
  440.                 'votes': votes,
  441.                 'is_deleted': is_deleted,
  442.                 'parent_id': parent_id
  443.             }
  444.  
  445.             comments.append(comment)
  446.  
  447.         return comments
  448.  
  449.     def _parse_article_html(self, html_content):
  450.         """
  451.        게시글 HTML 내용을 파싱하여 게시글 정보를 dictionary 형태로 반환합니다.
  452.  
  453.        Args:
  454.            html_content: 게시글 HTML 내용
  455.  
  456.        Returns:
  457.            dict: 게시글 정보 (제목, 작성자, 조회수, 좋아요 수)
  458.        """
  459.         import re
  460.         soup = BeautifulSoup(html_content, 'html.parser')
  461.  
  462.         # 제목 추출
  463.         title_element = soup.select_one('h1.text-2xl.font-bold')
  464.         title = title_element.text.strip() if title_element else None
  465.  
  466.         # 작성자 추출
  467.         author_element = soup.select_one('a.text-sm.font-medium[href^="/u/"]')
  468.         author = author_element.text.strip() if author_element else None
  469.  
  470.         # 조회수와 좋아요 수 추출
  471.         stats_element = soup.select_one('div.text-xs.text-zinc-400')
  472.  
  473.         views = None
  474.         likes = None
  475.  
  476.         if stats_element:
  477.             stats_text = stats_element.text.strip()
  478.             views_match = re.search(r'조회\s+(\d+)', stats_text)
  479.             likes_match = re.search(r'좋아요\s+(\d+)', stats_text)
  480.  
  481.             if views_match:
  482.                 views = int(views_match.group(1))
  483.             if likes_match:
  484.                 likes = int(likes_match.group(1))
  485.  
  486.         # 게시글 내용 추출 및 HTML 태그 제거
  487.         content_element = soup.select_one('div.prose.prose-invert.max-w-none')
  488.         content = None
  489.  
  490.         if content_element:
  491.             # 줄바꿈 태그를 먼저 \n으로 대체
  492.             for br in content_element.find_all('br'):
  493.                 br.replace_with('\n')
  494.  
  495.             # h1 태그는 제목으로 강조 처리
  496.             for h1 in content_element.find_all('h1'):
  497.                 # 제목 앞뒤로 개행 추가하고 강조
  498.                 h1_text = h1.get_text(strip=True)
  499.                 h1.replace_with(f'\n\n{h1_text}\n')
  500.  
  501.             # 텍스트만 추출
  502.             content = content_element.get_text()
  503.  
  504.             # 여러 개의 연속된 줄바꿈을 최대 2개로 제한
  505.             content = re.sub(r'\n{3,}', '\n\n', content)
  506.  
  507.             # 앞뒤 공백 제거
  508.             content = content.strip()
  509.  
  510.         return {
  511.             'title': title,
  512.             'author': author,
  513.             'views': views,
  514.             'likes': likes,
  515.             'content': content
  516.         }
  517.  
  518.     def get_article_info(self, article_id, rsc=None, parse_comments=True):
  519.         """
  520.        게시글 정보와 댓글을 가져옵니다.
  521.  
  522.        Args:
  523.            article_id: 게시글 ID
  524.            rsc: _rsc 쿼리 매개변수 (선택 사항)
  525.            parse_comments: 댓글 파싱 여부 (기본값: True)
  526.  
  527.        Returns:
  528.            dict: 게시글 정보 (제목, 작성자, 조회수, 좋아요 수, 내용, 댓글 목록)
  529.        """
  530.         response = self.get_article(article_id, rsc)
  531.  
  532.         if response.status_code != 200:
  533.             raise Exception(f"게시글 가져오기 실패: {response.status_code}")
  534.  
  535.         article_info = self._parse_article_html(response.text)
  536.  
  537.         # 댓글 파싱 옵션이 활성화된 경우 댓글 정보도 추가
  538.         if parse_comments:
  539.             comments = self._parse_comments_html(response.text)
  540.             article_info['comments'] = comments
  541.  
  542.         return article_info
  543.  
  544.     def get_user_profile(self, handle: str, rsc: Optional[str] = None) -> requests.Response:
  545.         """사용자 프로필 페이지 가져오기
  546.  
  547.        Args:
  548.            handle: 사용자 핸들(아이디)
  549.            rsc: _rsc 쿼리 매개변수 (선택 사항)
  550.        """
  551.         url = f"{self.base_url}/u/{handle}"
  552.         params = {}
  553.         if rsc:
  554.             params["_rsc"] = rsc
  555.         return self._request("GET", url, params=params)
  556.  
  557.  
  558. class KoneClient:
  559.     """Kone API의 통합 클라이언트"""
  560.  
  561.     def __init__(self, secure_neko_cookie: str, sub_name: str = "kone"):
  562.         """
  563.        Args:
  564.            secure_neko_cookie: __Secure_Neko 쿠키 값
  565.            sub_name: 서브 이름 (기본값: trickcalrevive)
  566.        """
  567.         self.api = KoneAPI(secure_neko_cookie, sub_name)
  568.         self.web = KoneWebAPI(secure_neko_cookie, sub_name)
Advertisement
Add Comment
Please, Sign In to add comment