Repository: PttCodingMan/PyPtt Branch: master Commit: f277f1adf595 Files: 119 Total size: 328.8 KB Directory structure: gitextract_7t96yf9r/ ├── .github/ │ └── workflows/ │ ├── deploy.yml │ ├── docs.yml │ └── test.yml ├── .gitignore ├── GourceScript.bat ├── LICENSE ├── MANIFEST.in ├── PyPtt/ │ ├── PTT.py │ ├── __init__.py │ ├── _api_bucket.py │ ├── _api_call_status.py │ ├── _api_change_pw.py │ ├── _api_comment.py │ ├── _api_del_post.py │ ├── _api_get_board_info.py │ ├── _api_get_board_list.py │ ├── _api_get_bottom_post_list.py │ ├── _api_get_favourite_board.py │ ├── _api_get_newest_index.py │ ├── _api_get_post.py │ ├── _api_get_post_index.py │ ├── _api_get_time.py │ ├── _api_get_user.py │ ├── _api_give_money.py │ ├── _api_has_new_mail.py │ ├── _api_loginout.py │ ├── _api_mail.py │ ├── _api_mark_post.py │ ├── _api_post.py │ ├── _api_reply_post.py │ ├── _api_search_user.py │ ├── _api_set_board_title.py │ ├── _api_util.py │ ├── check_value.py │ ├── command.py │ ├── config.py │ ├── connect_core.py │ ├── data_type.py │ ├── exceptions.py │ ├── i18n.py │ ├── lang/ │ │ ├── en_US.yaml │ │ └── zh_TW.yaml │ ├── lib_util.py │ ├── log.py │ ├── screens.py │ └── service.py ├── README.md ├── docs/ │ ├── CNAME │ ├── Makefile │ ├── api/ │ │ ├── bucket.rst │ │ ├── change_pw.rst │ │ ├── comment.rst │ │ ├── del_mail.rst │ │ ├── del_post.rst │ │ ├── get_aid_from_url.rst │ │ ├── get_all_boards.rst │ │ ├── get_board_info.rst │ │ ├── get_bottom_post_list.rst │ │ ├── get_favourite_boards.rst │ │ ├── get_mail.rst │ │ ├── get_newest_index.rst │ │ ├── get_post.rst │ │ ├── get_time.rst │ │ ├── get_user.rst │ │ ├── give_money.rst │ │ ├── index.rst │ │ ├── init.rst │ │ ├── login_logout.rst │ │ ├── mail.rst │ │ ├── mark_post.rst │ │ ├── post.rst │ │ ├── reply_post.rst │ │ ├── search_user.rst │ │ └── set_board_title.rst │ ├── changelog.rst │ ├── conf.py │ ├── dev.rst │ ├── docker.rst │ ├── examples.rst │ ├── exceptions.rst │ ├── faq.rst │ ├── index.rst │ ├── install.rst │ ├── make.bat │ ├── requirements.txt │ ├── roadmap.rst │ ├── robots.txt │ ├── service.rst │ └── type.rst ├── make_doc.sh ├── requirements.txt ├── scripts/ │ ├── lang.py │ └── package_script.py ├── setup.py ├── tests/ │ ├── change_pw.py │ ├── comment.py │ ├── config.py │ ├── exceptions.py │ ├── get_board_info.py │ ├── get_board_list.py │ ├── get_bottom_post_list.py │ ├── get_favourite_boards.py │ ├── get_mail.py │ ├── get_newest_index.py │ ├── get_post.py │ ├── get_time.py │ ├── get_user.py │ ├── give_p.py │ ├── i18n.py │ ├── init.py │ ├── logger.py │ ├── login_logout.py │ ├── performance.py │ ├── post.py │ ├── reply.py │ ├── search_user.py │ ├── service.py │ └── util.py └── upload.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/deploy.yml ================================================ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: deploy # run on merge to master or manual trigger on: pull_request: types: [ closed ] branches: - master paths: - 'PyPtt/*.py' - 'setup.py' workflow_dispatch: jobs: deploy: name: Deploy to PyPI and Docker runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: | python -m build - name: Publish package to TestPyPI if: github.event_name == 'workflow_dispatch' && github.event.pull_request.merged == false uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.TEST_PYPI_API_TOKEN }} repository_url: https://test.pypi.org/legacy/ - name: Publish package if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Trigger Docker build if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true uses: InformaticsMatters/trigger-ci-action@1.0.1 with: ci-owner: PyPtt ci-repository: PyPtt_image ci-user: PttCodingMan ci-ref: refs/heads/main ci-user-token: ${{ secrets.ACCESS_TOKEN }} ci-name: build PyPtt image ================================================ FILE: .github/workflows/docs.yml ================================================ name: docs # run on merge to master or manual trigger on: pull_request: types: [ closed ] paths: - 'docs/**/*' - 'PyPtt/*.py' workflow_dispatch: jobs: build: if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true name: Build doc and Deploy runs-on: ubuntu-latest steps: - uses: actions/setup-python@v5 - uses: actions/checkout@v4 with: fetch-depth: 0 # otherwise, you will failed to push refs to dest repo - name: Build and Commit uses: sphinx-notes/pages@v2 with: requirements_path: docs/requirements.txt - name: Push changes uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: gh-pages ================================================ FILE: .github/workflows/test.yml ================================================ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: test # run on every PR creation, or manual trigger # run on every push to non-master branch on: pull_request: types: - opened push: branches-ignore: - 'master' workflow_dispatch: env: DEP_PATH: requirements.txt jobs: check: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 PyPtt/ --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 PyPtt/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test run: | python tests/init.py scan: runs-on: ubuntu-latest steps: - name: Check out master uses: actions/checkout@v4 - name: detect-secrets uses: reviewdog/action-detect-secrets@master with: reporter: github-pr-review - name: Security vulnerabilities scan uses: aufdenpunkt/python-safety-check@master with: scan_requirements_file_only: true ================================================ FILE: .gitignore ================================================ __pycache__/ build/ dist/ /CrawlBoardResult.txt /.pypirc PTTLibrary.egg-info/ *Out.txt /Big5Data.txt PTTLibrary-*/ /PTTLibrary/i18n.txt /Test.txt Account*.txt *.spec /LogHandler.txt .vscode .idea/ venv/ /log.txt PyPtt.egg-info/ /test_account*.txt /test_result.txt *.json tests/ptt.sh docs/_build/ .DS_Store test*.py ptt.sh ================================================ FILE: GourceScript.bat ================================================ @echo off cls gource --seconds-per-day 0.05 --title "PTT Library" ================================================ FILE: LICENSE ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ================================================ FILE: MANIFEST.in ================================================ include LICENSE include README.md # Include the data files recursive-include PyPtt * ================================================ FILE: PyPtt/PTT.py ================================================ from __future__ import annotations import functools import threading from typing import Dict, Tuple, Callable, List, Optional, Any from . import __version__ from . import _api_bucket from . import _api_change_pw from . import _api_comment from . import _api_del_post from . import _api_get_board_info from . import _api_get_board_list from . import _api_get_bottom_post_list from . import _api_get_favourite_board from . import _api_get_newest_index from . import _api_get_post from . import _api_get_time from . import _api_get_user from . import _api_give_money from . import _api_loginout from . import _api_mail from . import _api_mark_post from . import _api_post from . import _api_reply_post from . import _api_search_user from . import _api_set_board_title from . import check_value from . import config from . import connect_core from . import data_type from . import i18n from . import lib_util from . import log class API: def __init__(self, **kwargs): """ 初始化 PyPtt。 Args: language (:ref:`language`): PyPtt 顯示訊息的語言。預設為 **MANDARIN**。 log_level (LogLevel_): PyPtt 顯示訊息的等級。預設為 **INFO**。 screen_timeout (int): 經過 screen_timeout 秒之後, PyPtt 將會判定無法判斷目前畫面的狀況。預設為 **3 秒**。 screen_long_timeout (int): 經過 screen_long_timeout 秒之後,PyPtt 將會判定無法判斷目前畫面的狀況,這會用在較長的等待時間,例如踢掉其他連線等等。預設為 **10 秒**。 screen_post_timeout (int): 經過 screen_post_timeout 秒之後,PyPtt 將會判定無法判斷目前畫面的狀況,這會用在較長的等待時間,例如發佈文章等等。預設為 **60 秒**。 connect_mode (:ref:`connect-mode`): PyPtt 連線的模式。預設為 **WEBSOCKETS**。 logger_callback (Callable): PyPtt 顯示訊息的 callback。預設為 None。 port (int): PyPtt 連線的 port。預設為 **23**。 host (:ref:`host`): PyPtt 連線的 PTT 伺服器。預設為 **PTT1**。 check_update (bool): 是否檢查 PyPtt 的更新。預設為 **True**。 Returns: None 範例:: import PyPtt ptt_bot = PyPtt.API() 參考: :ref:`language`、LogLevel_、:ref:`connect-mode`、:ref:`host` .. _LogLevel: https://github.com/PttCodingMan/SingleLog/blob/d7c19a1b848dfb1c9df8201f13def9a31afd035c/SingleLog/SingleLog.py#L22 英文顯示範例:: import PyPtt ptt_bot = PyPtt.API( language=PyPtt.Language.ENGLISH) 除錯範例:: import PyPtt ptt_bot = PyPtt.API( log_level=PyPtt.LogLevel.DEBUG) """ log_level = kwargs.get('log_level', log.INFO) if not isinstance(log_level, log.LogLv): raise TypeError('[PyPtt] log_level must be log.Level') logger_callback = kwargs.get('logger_callback', None) log.init(log_level, logger_callback=logger_callback) language = kwargs.get('language', data_type.Language.MANDARIN) if not isinstance(language, str): raise TypeError('[PyPtt] language must be PyPtt.Language') if language not in i18n.locale_pool: raise TypeError('[PyPtt] language must be PyPtt.Language') self.config = config.Config() self.config.log_level = log_level self.config.language = language print('language', self.config.language) i18n.init(self.config.language) self.is_mailbox_full: bool = False self.is_registered_user: bool = False self.process_picks: int = 0 self.ptt_id: str = '' self._ptt_pw: str = '' self._is_login: bool = False host = kwargs.get('host', data_type.HOST.PTT1) screen_timeout = kwargs.get('screen_timeout', 3.0) screen_long_timeout = kwargs.get('screen_long_timeout', 10.0) screen_post_timeout = kwargs.get('screen_post_timeout', 60.0) check_value.check_type(host, (data_type.HOST, str), 'host') check_value.check_type(screen_timeout, float, 'screen_timeout') check_value.check_type(screen_long_timeout, float, 'screen_long_timeout') check_value.check_type(screen_post_timeout, float, 'screen_post_timeout') if screen_timeout != 0: self.config.screen_timeout = screen_timeout if screen_long_timeout != 0: self.config.screen_long_timeout = screen_long_timeout if screen_post_timeout != 0: self.config.screen_post_timeout = screen_post_timeout self.config.host = host self.host = host port = kwargs.get('port', 23) check_value.check_type(port, int, 'port') check_value.check_range(port, 1, 65535 - 1, 'port') self.config.port = port connect_mode = kwargs.get('connect_mode', data_type.ConnectMode.WEBSOCKETS) check_value.check_type(connect_mode, data_type.ConnectMode, 'connect_mode') if host in [data_type.HOST.PTT1, data_type.HOST.PTT2] and connect_mode is data_type.ConnectMode.TELNET: raise ValueError('[PyPtt] TELNET is not available on PTT1 and PTT2') self.config.connect_mode = connect_mode self.connect_core = connect_core.API(self.config) self._exist_board_list = [] self._moderators = dict() self._thread_id = threading.get_ident() self._goto_board_list = [] self._board_info_list = dict() self._newest_index_data = data_type.TimedDict(timeout=2) log.logger.debug('thread_id', self._thread_id) log.logger.info( i18n.replace(i18n.welcome, __version__)) log.logger.info('PyPtt', i18n.initialization) if self.config.connect_mode == data_type.ConnectMode.TELNET: log.logger.info(i18n.set_connect_mode, '...', i18n.connect_mode_TELNET) elif self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS: log.logger.info(i18n.set_connect_mode, '...', i18n.connect_mode_WEBSOCKET) if self.config.language == data_type.Language.MANDARIN: log.logger.info(i18n.set_up_lang_module, '...', i18n.mandarin_module) elif self.config.language == data_type.Language.ENGLISH: log.logger.info(i18n.set_up_lang_module, '...', i18n.english_module) if self.config.host == data_type.HOST.PTT1: log.logger.info(i18n.set_connect_host, '...', i18n.PTT) elif self.config.host == data_type.HOST.PTT2: log.logger.info(i18n.set_connect_host, '...', i18n.PTT2) elif self.config.host == data_type.HOST.LOCALHOST: log.logger.info(i18n.set_connect_host, '...', i18n.localhost) else: log.logger.info(i18n.set_connect_host, '...', self.config.host) log.logger.info('PyPtt', i18n.initialization, '...', i18n.done) check_update = kwargs.get('check_update', True) check_value.check_type(check_update, bool, 'check_update') if check_update: version_compare, remote_version = lib_util.sync_version() if version_compare is data_type.Compare.SMALLER: log.logger.info(i18n.current_version, __version__) log.logger.info(i18n.new_version, remote_version) elif version_compare is data_type.Compare.BIGGER: log.logger.info(i18n.development_version, __version__) else: log.logger.info(i18n.latest_version, __version__) else: log.logger.info(i18n.current_version, __version__) def __del__(self): if log.logger: log.logger.debug(i18n.goodbye) def login(self, ptt_id: str, ptt_pw: str, kick_other_session: bool = False) -> None: """ 登入 PTT。 Args: ptt_id (str): PTT ID。 ptt_pw (str): PTT 密碼。 kick_other_session (bool): 是否踢掉其他登入的 session。預設為 False。 Returns: None Raises: LoginError: 登入失敗。 WrongIDorPassword: 帳號或密碼錯誤。 OnlySecureConnection: 只能使用安全連線。 ResetYourContactEmail: 請先至信箱設定連絡信箱。 範例:: import PyPtt ptt_bot = PyPtt.API() try: ptt_bot.login( ptt_id='ptt_id', ptt_pw='ptt_pw', kick_other_session=True) except PyPtt.LoginError: print('登入失敗') except PyPtt.WrongIDorPassword: print('帳號密碼錯誤') except PyPtt.OnlySecureConnection: print('只能使用安全連線') except PyPtt.ResetYourContactEmail: print('請先至信箱設定連絡信箱') """ _api_loginout.login(self, ptt_id, ptt_pw, kick_other_session) def logout(self) -> None: """ 登出 PTT。 Returns: None 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. # .. do something .. finally: ptt_bot.logout() """ _api_loginout.logout(self) def get_time(self) -> str: """ 取得 PTT 系統時間。 Returns: None 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. time = ptt_bot.get_time() # .. do something .. finally: ptt_bot.logout() """ return _api_get_time.get_time(self) def get_post(self, board: str, aid: Optional[str] = None, index: Optional[int] = None, search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None, search_list: Optional[List[tuple]] = None, query: bool = False) -> Dict: """ 取得文章。 Args: board (str): 看板名稱。 aid (str): 文章編號。 index: 文章編號。 search_list (List[str]): 搜尋清單。 query (bool): 是否為查詢模式。 Returns: Dict,文章內容。詳見 :ref:`post-field` Raises: RequireLogin: 需要登入。 NoSuchBoard: 看板不存在。 使用 AID 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. post_info = ptt_bot.get_post('Python', aid='1TJH_XY0') # .. do something .. finally: ptt_bot.logout() 使用 index 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. post_info = ptt_bot.get_post('Python', index=1) # .. do something .. finally: ptt_bot.logout() 使用搜尋範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. post_info = ptt_bot.get_post( 'Python', index=1, search_list=[(PyPtt.SearchType.KEYWORD, 'PyPtt')] ) # .. do something .. finally: ptt_bot.logout() | 更多範例參考 :ref:`取得文章 ` | 參考 :ref:`取得最新文章編號 ` """ return _api_get_post.get_post( self, board, aid=aid, index=index, search_type=search_type, search_condition=search_condition, search_list=search_list, query=query) def get_newest_index(self, index_type: data_type.NewIndex, board: Optional[str] = None, search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None, search_list: Optional[List[Tuple[Any | str]]] = None, ) -> int: """ 取得最新文章或信箱編號。 Args: index_type (:ref:`new-index`): 編號類型。 board (str): 看板名稱。 search_list (List[str]): 搜尋清單。 Returns: int,最新文章或信箱編號。 Raises: RequireLogin: 需要登入。 NoSuchBoard: 看板不存在。 取得最新看板編號:: import PyPtt ptt_bot = PyPtt.API() # get newest index of board try: # .. login .. newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, 'Python') # .. do something .. finally: ptt_bot.logout() 取得最新文章編號使用搜尋:: import PyPtt ptt_bot = PyPtt.API() search_list = [(PyPtt.SearchType.KEYWORD, 'PyPtt')] try: # .. login .. newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, 'Python', search_list=search_list) # .. do something .. finally: ptt_bot.logout() 取得最新信箱編號:: import PyPtt ptt_bot = PyPtt.API() # get newest index of mail try: # .. login .. newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.MAIL) # .. do something .. finally: ptt_bot.logout() 參考 :ref:`搜尋編號種類 `、:ref:`取得文章 ` """ return _api_get_newest_index.get_newest_index( self, index_type, board, search_type, search_condition, search_list) def post(self, board: str, title_index: int, title: str, content: str, sign_file: [str | int] = 0) -> None: """ 發文。 Args: board (str): 看板名稱。 title_index (int): 文章標題編號。 title (str): 文章標題。 content (str): 文章內容。 sign_file (str | int): 編號或隨機簽名檔 (x),預設為 0 (不選)。 Returns: None Raises: UnregisteredUser: 未註冊使用者。 RequireLogin: 需要登入。 NoSuchBoard: 看板不存在。 NoPermission: 沒有發佈權限。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content='測試內容', sign_file=0) # .. do something .. finally: ptt_bot.logout() """ _api_post.post(self, board, title, content, title_index, sign_file) def comment(self, board: str, comment_type: data_type.CommentType, content: str, aid: Optional[str] = None, index: int = 0) -> None: """ 推文。 Args: board (str): 看板名稱。 comment_type (:ref:`comment-type`): 推文類型。 content (str): 推文內容。 aid (str): 文章編號。 index (int): 文章編號。 Returns: None Raises: UnregisteredUser: 未註冊使用者。 RequireLogin: 需要登入。 NoSuchBoard: 看板不存在。 NoSuchPost: 文章不存在。 NoPermission: 沒有推文權限。 NoFastComment: 推文間隔太短。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.PUSH, content='Comment by index', index=123) ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.PUSH, content='Comment by index', aid='17MrayxF') # .. do something .. finally: ptt_bot.logout() 參考 :ref:`推文類型 `、:ref:`取得最新文章編號 ` """ _api_comment.comment(self, board, comment_type, content, aid, index) def get_user(self, user_id: str) -> Dict: """ 取得使用者資訊。 Args: user_id (str): 使用者 ID。 Returns: Dict,使用者資訊。詳見 :ref:`使用者資料欄位 ` Raises: RequireLogin: 需要登入。 NoSuchUser: 使用者不存在。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. user_info = ptt_bot.get_user('CodingMan') # .. do something .. finally: ptt_bot.logout() 參考 :ref:`使用者資料欄位 ` """ return _api_get_user.get_user(self, user_id) def give_money(self, ptt_id: str, money: int, red_bag_title: Optional[str] = None, red_bag_content: Optional[str] = None) -> None: """ 轉帳,詳見 `P 幣`_。 .. _`P 幣`: https://pttpedia.fandom.com/zh/wiki/P%E5%B9%A3 Args: ptt_id (str): PTT ID。 money (int): 轉帳金額。 red_bag_title (str): 紅包標題。 red_bag_content (str): 紅包內容。 Returns: None Raises: RequireLogin: 需要登入。 UnregisteredUser: 未註冊使用者。 NoSuchUser: 使用者不存在。 NoMoney: 餘額不足。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.give_money(ptt_id='CodingMan', money=100) # or ptt_bot.give_money('CodingMan', 100, red_bag_title='紅包袋標題', red_bag_content='紅包袋內文') # .. do something .. finally: ptt_bot.logout() """ _api_give_money.give_money(self, ptt_id, money, red_bag_title, red_bag_content) def mail(self, ptt_id: str, title: str, content: str, sign_file: [int | str] = 0, backup: bool = True) -> None: """ 寄信。 Args: ptt_id (str): PTT ID。 title (str): 信件標題。 content (str): 信件內容。 sign_file (str | int): 編號或隨機簽名檔 (x),預設為 0 (不選)。 backup (bool): 如果是 True 寄信時將會備份信件,預設為 True。 Returns: None Raises: RequireLogin: 需要登入。 UnregisteredUser: 未註冊使用者。 NoSuchUser: 使用者不存在。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.mail(ptt_id='CodingMan', title='信件標題', content='信件內容') # .. do something .. finally: ptt_bot.logout() """ _api_mail.mail(self, ptt_id, title, content, sign_file, backup) def get_all_boards(self) -> List[str]: """ 取得全站看板清單。 Returns: List[str],看板清單。 Raises: RequireLogin: 需要登入。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. board_list = ptt_bot.get_all_boards() # .. do something .. finally: ptt_bot.logout() """ return _api_get_board_list.get_board_list(self) def reply_post(self, reply_to: data_type.ReplyTo, board: str, content: str, sign_file: [str | int] = 0, aid: Optional[str] = None, index: int = 0) -> None: """ 回覆文章。 Args: reply_to (:ref:`reply-to`): 回覆類型。 board (str): 看板名稱。 content (str): 回覆內容。 sign_file (str | int): 編號或隨機簽名檔 (x),預設為 **0** (不選)。 aid: 文章編號。 index: 文章編號。 Returns: None Raises: RequireLogin: 需要登入。 NoSuchBoard: 看板不存在。 NoSuchPost: 文章不存在。 NoPermission: 沒有回覆權限。 CantResponse: 已結案並標記, 不得回應。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.reply_post(reply_to=PyPtt.ReplyTo.BOARD, board='Test', content='PyPtt 程式回覆測試', index=123) # .. do something .. finally: ptt_bot.logout() 參考 :ref:`回覆類型 `、:ref:`取得最新文章編號 ` """ _api_reply_post.reply_post(self, reply_to, board, content, sign_file, aid, index) def set_board_title(self, board: str, new_title: str) -> None: """ 設定看板標題。 Args: board (str): 看板名稱。 new_title (str): 新標題。 Returns: None Raises: RequireLogin: 需要登入。 UnregisteredUser: 未註冊使用者。 NoSuchBoard: 看板不存在。 NeedModeratorPermission: 需要看板管理員權限。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.set_board_title(board='Test', new_title='現在時間 %s' % datetime.datetime.now()) # .. do something .. finally: ptt_bot.logout() """ _api_set_board_title.set_board_title(self, board, new_title) def mark_post(self, mark_type: int, board: str, aid: Optional[str] = None, index: int = 0, search_type: int = 0, search_condition: Optional[str] = None) -> None: """ 標記文章。 Args: mark_type (:ref:`mark-type`): 標記類型。 board (str): 看板名稱。 aid (str): 文章編號。 index (int): 文章編號。 search_type (:ref:`search-type`): 搜尋類型。 search_condition (str): 搜尋條件。 Returns: None Raises: RequireLogin: 需要登入。 UnregisteredUser: 未註冊使用者。 NoSuchBoard: 看板不存在。 NoSuchPost: 文章不存在。 NeedModeratorPermission: 需要看板管理員權限。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.mark_post(mark_type=PyPtt.MarkType.M, board='Test', index=123) # .. do something .. finally: ptt_bot.logout() """ _api_mark_post.mark_post(self, mark_type, board, aid, index, search_type, search_condition) def get_favourite_boards(self) -> List[dict]: """ 取得我的最愛清單。 Returns: List[dict],收藏看板清單,詳見 :ref:`favorite-board-field`。 Raises: RequireLogin: 需要登入。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. favourite_boards = ptt_bot.get_favourite_boards() # .. do something .. finally: ptt_bot.logout() """ return _api_get_favourite_board.get_favourite_board(self) def bucket(self, board: str, bucket_days: int, reason: str, ptt_id: str) -> None: """ 水桶。 Args: board (str): 看板名稱。 bucket_days (int): 水桶天數。 reason (str): 水桶原因。 ptt_id (str): PTT ID。 Returns: None Raises: RequireLogin: 需要登入。 UnregisteredUser: 未註冊使用者。 NoSuchBoard: 看板不存在。 NoSuchUser: 使用者不存在。 NeedModeratorPermission: 需要看板管理員權限。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.bucket(board='Test', bucket_days=7, reason='PyPtt 程式水桶測試', ptt_id='test') # .. do something .. finally: ptt_bot.logout() """ _api_bucket.bucket(self, board, bucket_days, reason, ptt_id) def search_user(self, ptt_id: str, min_page: Optional[int] = None, max_page: Optional[int] = None) -> List[str]: """ 搜尋使用者。 Args: ptt_id (str): PTT ID。 min_page (int): 最小頁數。 max_page (int): 最大頁數。 Returns: List[str],搜尋結果。 Raises: RequireLogin: 需要登入。 UnregisteredUser: 未註冊使用者。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. search_result = ptt_bot.search_user(ptt_id='Coding') # .. do something .. finally: ptt_bot.logout() """ return _api_search_user.search_user(self, ptt_id, min_page, max_page) def get_board_info(self, board: str, get_post_types: bool = False) -> Dict: """ 取得看板資訊。 Args: board (str): 看板名稱。 get_post_types (bool): 是否取得文章類型,例如:八卦板的「問卦」。 Returns: Dict,看板資訊,詳見 :ref:`board-field`。 Raises: RequireLogin: 需要登入。 NoSuchBoard: 看板不存在。 NoPermission: 沒有權限。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. board_info = ptt_bot.get_board_info(board='Test') # .. do something .. finally: ptt_bot.logout() """ return _api_get_board_info.get_board_info(self, board, get_post_types, call_by_others=False) def get_mail(self, index: int, search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None, search_list: Optional[list] = None) -> Dict: """ 取得信件。 Args: index (int): 信件編號。 search_type (:ref:`search-type`): 搜尋類型。 search_condition: 搜尋條件。 search_list: 搜尋清單。 Returns: Dict,信件資訊,詳見 :ref:`mail-field`。 Raises: RequireLogin: 需要登入。 NoSuchMail: 信件不存在。 NoPermission: 沒有權限。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. mail = ptt_bot.get_mail(index=1) # .. do something .. finally: ptt_bot.logout() 參考 :doc:`get_newest_index` """ return _api_mail.get_mail(self, index, search_type, search_condition, search_list) def del_mail(self, index: int) -> None: """ 刪除信件。 Args: index (int): 信件編號。 Returns: None Raises: RequireLogin: 需要登入。 UnregisteredUser: 未註冊使用者。 MailboxFull: 信箱已滿。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.del_mail(index=1) # .. do something .. finally: ptt_bot.logout() 參考 :doc:`get_newest_index` """ _api_mail.del_mail(self, index) def change_pw(self, new_password: str) -> None: """ 更改密碼。 備註:因批踢踢系統限制,最長密碼為 8 碼。 Args: new_password (str): 新密碼。 Returns: None Raises: RequireLogin: 需要登入。 SetContactMailFirst: 需要先設定聯絡信箱。 WrongPassword: 密碼錯誤。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.change_pw(new_password='123456') # .. do something .. finally: ptt_bot.logout() """ _api_change_pw.change_pw(self, new_password) @functools.lru_cache(maxsize=64) def get_aid_from_url(self, url: str) -> Tuple[str, str]: """ 從網址取得看板名稱與文章編號。 Args: url: 網址。 Returns: Tuple[str, str],看板名稱與文章編號。 範例:: import PyPtt ptt_bot = PyPtt.API() url = 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html' board, aid = ptt_bot.get_aid_from_url(url) """ return lib_util.get_aid_from_url(url) def get_bottom_post_list(self, board: str) -> List[str]: """ 取得看板置底文章清單。 Args: board (str): 看板名稱。 Returns: List[post],置底文章清單,詳見 :ref:`post-field`。 Raises: RequireLogin: 需要登入。 NoSuchBoard: 看板不存在。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. bottom_post_list = ptt_bot.get_bottom_post_list(board='Python') # .. do something .. finally: ptt_bot.logout() """ return _api_get_bottom_post_list.get_bottom_post_list(self, board) def del_post(self, board: str, aid: Optional[str] = None, index: int = 0) -> None: """ 刪除文章。 Args: board (str): 看板名稱。 aid (str): 文章編號。 index (int): 文章編號。 Returns: None Raises: RequireLogin: 需要登入。 UnregisteredUser: 未註冊使用者。 NoSuchBoard: 看板不存在。 NoSuchPost: 文章不存在。 NoPermission: 沒有權限。 範例:: import PyPtt ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.del_post(board='Python', aid='1TJH_XY0') # .. do something .. finally: ptt_bot.logout() """ _api_del_post.del_post(self, board, aid, index) def fast_post_step0(self, board: str, title: str, content: str, post_type: int) -> None: _api_post.fast_post_step0(self, board, title, content, post_type) def fast_post_step1(self, sign_file): _api_post.fast_post_step1(self, sign_file) if __name__ == '__main__': print('PyPtt v ' + __version__) print('Maintained by CodingMan') ================================================ FILE: PyPtt/__init__.py ================================================ __version__ = '1.1.2' from .PTT import API from .data_type import * from .exceptions import * from .log import LogLevel from .service import Service LOG_LEVEL = LogLevel _main_version = '1.2' ================================================ FILE: PyPtt/_api_bucket.py ================================================ from . import _api_util from . import check_value from . import command from . import connect_core from . import exceptions from . import i18n from . import lib_util from . import screens def bucket(api, board: str, bucket_days: int, reason: str, ptt_id: str) -> None: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) check_value.check_type(board, str, 'board') check_value.check_type(bucket_days, int, 'bucket_days') check_value.check_type(reason, str, 'reason') check_value.check_type(ptt_id, str, 'ptt_id') api.get_user(ptt_id) _api_util.check_board(api, board, check_moderator=True) _api_util.goto_board(api, board) cmd_list = [] cmd_list.append('i') cmd_list.append(command.ctrl_p) cmd_list.append('w') cmd_list.append(command.enter) cmd_list.append('a') cmd_list.append(command.enter) cmd_list.append(ptt_id) cmd_list.append(command.enter) cmd = ''.join(cmd_list) cmd_list = [] cmd_list.append(str(bucket_days)) cmd_list.append(command.enter) cmd_list.append(reason) cmd_list.append(command.enter) cmd_list.append('y') cmd_list.append(command.enter) cmd_part2 = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('◆ 使用者之前已被禁言', exceptions_=exceptions.UserHasPreviouslyBeenBanned()), connect_core.TargetUnit('請以數字跟單位(預設為天)輸入期限', response=cmd_part2), connect_core.TargetUnit('其它鍵結束', response=command.enter), connect_core.TargetUnit('權限設定系統', response=command.enter), connect_core.TargetUnit('任意鍵', response=command.space), connect_core.TargetUnit(screens.Target.InBoard, break_detect=True), ] api.connect_core.send( cmd, target_list) ================================================ FILE: PyPtt/_api_call_status.py ================================================ from . import command from . import connect_core from . import data_type from . import exceptions from . import log from . import screens def get_call_status(api) -> None: # log.py = DefaultLogger('api', api.config.log_level) cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append('A') cmd_list.append(command.right) cmd_list.append(command.left) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('[呼叫器]打開', log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit('[呼叫器]拔掉', log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit('[呼叫器]防水', log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit('[呼叫器]好友', log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit('[呼叫器]關閉', log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit('★', log_level=log.DEBUG, response=cmd), ] for i in range(2): index = api.connect_core.send(cmd, target_list) if index < 0: if i == 0: continue raise exceptions.UnknownError('UnknownError') if index == 0: return data_type.call_status.ON if index == 1: return data_type.call_status.UNPLUG if index == 2: return data_type.call_status.WATERPROOF if index == 3: return data_type.call_status.FRIEND if index == 4: return data_type.call_status.OFF ori_screen = api.connect_core.get_screen_queue()[-1] raise exceptions.UnknownError(ori_screen) def set_call_status(api, call_status) -> None: # 打開 -> 拔掉 -> 防水 -> 好友 -> 關閉 current_call_status = api._get_call_status() cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append(command.ctrl_u) cmd_list.append('p') cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit(screens.Target.InUserList, break_detect=True)] while current_call_status != call_status: api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_long_timeout) current_call_status = api._get_call_status() ================================================ FILE: PyPtt/_api_change_pw.py ================================================ from . import command, _api_util from . import connect_core from . import exceptions from . import i18n from . import log def change_pw(api, new_password: str) -> None: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) log.logger.info(i18n.change_pw) new_password = new_password[:8] cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append('U') cmd_list.append(command.enter) cmd_list.append('I') cmd_list.append(command.enter) cmd_list.append('2') cmd_list.append(command.enter) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('設定聯絡信箱後才能修改密碼', exceptions_=exceptions.SetContactMailFirst()), connect_core.TargetUnit('您輸入的密碼不正確', exceptions_=exceptions.WrongPassword()), connect_core.TargetUnit('請您確定(Y/N)?', response='Y' + command.enter), connect_core.TargetUnit('檢查新密碼', response=new_password + command.enter, max_match=1), connect_core.TargetUnit('設定新密碼', response=new_password + command.enter, max_match=1), connect_core.TargetUnit('輸入原密碼', response=api._ptt_pw + command.enter, max_match=1), connect_core.TargetUnit('設定個人資料與密碼', break_detect=True) ] index = api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_long_timeout) if index < 0: ori_screen = api.connect_core.get_screen_queue()[-1] raise exceptions.UnknownError(ori_screen) api._ptt_pw = new_password log.logger.info(i18n.change_pw, '...', i18n.success) ================================================ FILE: PyPtt/_api_comment.py ================================================ import collections import time from . import _api_util from . import check_value from . import command from . import connect_core from . import data_type from . import exceptions from . import i18n from . import lib_util from . import log from . import screens comment_option = [ None, data_type.CommentType.PUSH, data_type.CommentType.BOO, data_type.CommentType.ARROW, ] def _comment(api, board: str, push_type: data_type.CommentType, push_content: str, post_aid: str, post_index: int) -> None: _api_util.goto_board(api, board) cmd_list = [] if post_aid is not None: cmd_list.append(lib_util.check_aid(post_aid)) elif post_index != 0: cmd_list.append(str(post_index)) else: raise ValueError('post_aid and post_index cannot be None at the same time') cmd_list.append(command.enter) cmd_list.append(command.comment) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('您覺得這篇', log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit(f'→ {api.ptt_id}: ', log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit('加註方式', log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit('禁止快速連續推文', log_level=log.INFO, break_detect=True, exceptions_=exceptions.NoFastComment()), connect_core.TargetUnit('禁止短時間內大量推文', log_level=log.INFO, break_detect=True, exceptions_=exceptions.NoFastComment()), connect_core.TargetUnit('使用者不可發言', log_level=log.INFO, break_detect=True, exceptions_=exceptions.NoPermission(i18n.no_permission)), connect_core.TargetUnit('◆ 抱歉, 禁止推薦', log_level=log.INFO, break_detect=True, exceptions_=exceptions.CantComment()), ] index = api.connect_core.send( cmd, target_list) if index == -1: raise exceptions.UnknownError('unknown error in comment') log.logger.debug(i18n.has_comment_permission) cmd_list = [] if index == 0 or index == 1: push_option_line = api.connect_core.get_screen_queue()[-1] push_option_line = push_option_line.split('\n')[-1] log.logger.debug('comment option line', push_option_line) available_push_type = collections.defaultdict(lambda: False) first_available_push_type = None if '值得推薦' in push_option_line: available_push_type[data_type.CommentType.PUSH] = True if first_available_push_type is None: first_available_push_type = data_type.CommentType.PUSH if '只加→註解' in push_option_line: available_push_type[data_type.CommentType.ARROW] = True if first_available_push_type is None: first_available_push_type = data_type.CommentType.ARROW if '給它噓聲' in push_option_line: available_push_type[data_type.CommentType.BOO] = True if first_available_push_type is None: first_available_push_type = data_type.CommentType.BOO log.logger.debug('available_push_type', available_push_type) if available_push_type[push_type] is False: if first_available_push_type: push_type = first_available_push_type if True in available_push_type.values(): cmd_list.append(str(comment_option.index(push_type))) cmd_list.append(push_content) cmd_list.append(command.enter) cmd_list.append('y') cmd_list.append(command.enter) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True), ] api.connect_core.send( cmd, target_list) def comment(api, board: str, push_type: data_type.CommentType, push_content: str, post_aid: str, post_index: int) -> None: if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) check_value.check_type(board, str, 'board') if not isinstance(push_type, data_type.CommentType): raise TypeError(f'CommentType must be data_type.CommentType') check_value.check_type(push_content, str, 'push_content') if post_aid is not None: check_value.check_type(post_aid, str, 'aid') check_value.check_type(post_index, int, 'index') if len(board) == 0: raise ValueError(f'wrong parameter board: {board}') if post_index != 0 and isinstance(post_aid, str): raise ValueError('wrong parameter index and aid can\'t both input') if post_index == 0 and post_aid is None: raise ValueError('wrong parameter index or aid must input') if post_index != 0: newest_index = api.get_newest_index( data_type.NewIndex.BOARD, board=board) check_value.check_index('index', post_index, newest_index) _api_util.check_board(api, board) board_info = api._board_info_list[board.lower()] if board_info[data_type.BoardField.is_comment_record_ip]: log.logger.debug(i18n.record_ip) if board_info[data_type.BoardField.is_comment_aligned]: log.logger.debug(i18n.push_aligned) max_push_length = 32 else: log.logger.debug(i18n.not_push_aligned) max_push_length = 43 - len(api.ptt_id) else: log.logger.debug(i18n.not_record_ip) if board_info[data_type.BoardField.is_comment_aligned]: log.logger.debug(i18n.push_aligned) max_push_length = 46 else: log.logger.debug(i18n.not_push_aligned) max_push_length = 58 - len(api.ptt_id) push_content = push_content.strip() push_list = [] while push_content: index = 0 jump = 0 while len(push_content[:index].encode('big5uao', 'replace')) < max_push_length: if index == len(push_content): break if push_content[index] == '\n': jump = 1 break index += 1 push_list.append(push_content[:index]) push_content = push_content[index + jump:] push_list = filter(None, push_list) for comment in push_list: log.logger.info(i18n.comment) for _ in range(2): try: _comment(api, board, push_type, comment, post_aid=post_aid, post_index=post_index) break except exceptions.NoFastComment: # screens.show(api.config, api.connect_core.getScreenQueue()) log.logger.info(i18n.wait_for_no_fast_comment) time.sleep(5.2) log.logger.info(i18n.comment, '...', i18n.success) ================================================ FILE: PyPtt/_api_del_post.py ================================================ from __future__ import annotations from typing import Optional from . import _api_util from . import check_value from . import command from . import connect_core from . import data_type from . import exceptions from . import i18n from . import lib_util from . import log from . import screens def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0) -> None: _api_util.one_thread(api) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) check_value.check_type(board, str, 'board') if post_aid is not None: check_value.check_type(post_aid, str, 'PostAID') check_value.check_type(post_index, int, 'PostIndex') if len(board) == 0: raise ValueError(f'board error parameter: {board}') if post_index != 0 and isinstance(post_aid, str): raise ValueError('wrong parameter index and aid can\'t both input') if post_index == 0 and post_aid is None: raise ValueError('wrong parameter index or aid must input') if post_index != 0: newest_index = api.get_newest_index( data_type.NewIndex.BOARD, board=board) check_value.check_index( 'PostIndex', post_index, newest_index) log.logger.info(i18n.delete_post) board_info = _api_util.check_board(api, board) check_author = True for moderator in board_info[data_type.BoardField.moderators]: if api.ptt_id.lower() == moderator.lower(): check_author = False break log.logger.info(i18n.delete_post) post_info = api.get_post(board, aid=post_aid, index=post_index, query=True) if post_info[data_type.PostField.post_status] != data_type.PostStatus.EXISTS: # delete success log.logger.info(i18n.success) return if check_author: if api.ptt_id.lower() != post_info[data_type.PostField.author].lower(): log.logger.info(i18n.delete_post, '...', i18n.fail) raise exceptions.NoPermission(i18n.no_permission) _api_util.goto_board(api, board) cmd_list = [] if post_aid is not None: cmd_list.append(lib_util.check_aid(post_aid)) elif post_index != 0: cmd_list.append(str(post_index)) else: raise ValueError('post_aid and post_index cannot be None at the same time') cmd_list.append(command.enter) cmd_list.append('d') cmd = ''.join(cmd_list) api.confirm = False def confirm_delete_handler(screen): api.confirm = True target_list = [ connect_core.TargetUnit('請按任意鍵繼續', response=' '), connect_core.TargetUnit('請確定刪除(Y/N)?[N]', response='y' + command.enter, handler=confirm_delete_handler, max_match=1), connect_core.TargetUnit(screens.Target.InBoard, break_detect=True), ] index = api.connect_core.send( cmd, target_list) if index == 1: if not api.confirm: log.logger.info(i18n.delete_post, '...', i18n.fail) raise exceptions.NoPermission(i18n.no_permission) log.logger.info(i18n.delete_post, '...', i18n.success) ================================================ FILE: PyPtt/_api_get_board_info.py ================================================ import re from typing import Dict from . import _api_util from . import check_value from . import command from . import connect_core from . import exceptions from . import i18n from . import log from . import screens from .data_type import BoardField def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -> Dict: logger = log.init(log.DEBUG if call_by_others else log.INFO) _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) check_value.check_type(board, str, 'board') logger.info( i18n.replace(i18n.get_board_info, board)) _api_util.goto_board(api, board, refresh=True) ori_screen = api.connect_core.get_screen_queue()[-1] # print(ori_screen) nuser = None for line in ori_screen.split('\n'): if '編號' not in line: continue if '日 期' not in line: continue if '人氣' not in line: continue nuser = line break if nuser is None: raise exceptions.NoSuchBoard(api.config, board) # print('------------------------') # print('nuser', nuser) # print('------------------------') if '[靜]' in nuser: online_user = 0 else: if '編號' not in nuser or '人氣' not in nuser: raise exceptions.NoSuchBoard(api.config, board) pattern = re.compile('[\d]+') r = pattern.search(nuser) if r is None: raise exceptions.NoSuchBoard(api.config, board) # 減一是把自己本身拿掉 online_user = int(r.group(0)) - 1 logger.debug('人氣', online_user) target_list = [ connect_core.TargetUnit('任意鍵繼續', log_level=log.DEBUG if call_by_others else log.INFO, break_detect=True), ] api.connect_core.send( 'i', target_list) ori_screen = api.connect_core.get_screen_queue()[-1] # print(ori_screen) p = re.compile('《(.+)》看板設定') r = p.search(ori_screen) if r is not None: boardname = r.group(0)[1:-5].strip() logger.debug('看板名稱', boardname, board) if boardname.lower() != board.lower(): raise exceptions.NoSuchBoard(api.config, board) p = re.compile('中文敘述: (.+)') r = p.search(ori_screen) if r is not None: chinese_des = r.group(0)[5:].strip() logger.debug('中文敘述', chinese_des) p = re.compile('板主名單: (.+)') r = p.search(ori_screen) if r is not None: moderator_line = r.group(0)[5:].strip() if '(無)' in moderator_line: moderators = [] else: moderators = moderator_line.split('/') for moderator in moderators.copy(): if moderator == '徵求中': moderators.remove(moderator) logger.debug('板主名單', moderators) open_status = ('公開狀態(是否隱形): 公開' in ori_screen) logger.debug('公開狀態', open_status) into_top_ten_when_hide = ('隱板時 可以 進入十大排行榜' in ori_screen) logger.debug('隱板時可以進入十大排行榜', into_top_ten_when_hide) non_board_members_post = ('開放 非看板會員發文' in ori_screen) logger.debug('非看板會員發文', non_board_members_post) reply_post = ('開放 回應文章' in ori_screen) logger.debug('回應文章', reply_post) self_del_post = ('開放 自刪文章' in ori_screen) logger.debug('自刪文章', self_del_post) push_post = ('開放 推薦文章' in ori_screen) logger.debug('推薦文章', push_post) boo_post = ('開放 噓文' in ori_screen) logger.debug('噓文', boo_post) # 限制 快速連推文章, 最低間隔時間: 5 秒 # 開放 快速連推文章 fast_push = ('開放 快速連推文章' in ori_screen) logger.debug('快速連推文章', fast_push) if not fast_push: p = re.compile('最低間隔時間: [\d]+') r = p.search(ori_screen) if r is not None: min_interval = r.group(0)[7:].strip() min_interval = int(min_interval) else: min_interval = 0 logger.debug('最低間隔時間', min_interval) else: min_interval = 0 # 推文時 自動 記錄來源 IP # 推文時 不會 記錄來源 IP push_record_ip = ('推文時 自動 記錄來源 IP' in ori_screen) logger.debug('記錄來源 IP', push_record_ip) # 推文時 對齊 開頭 # 推文時 不用對齊 開頭 push_aligned = ('推文時 對齊 開頭' in ori_screen) logger.debug('對齊開頭', push_aligned) # 板主 可 刪除部份違規文字 moderator_can_del_illegal_content = ('板主 可 刪除部份違規文字' in ori_screen) logger.debug('板主可刪除部份違規文字', moderator_can_del_illegal_content) # 轉錄文章 會 自動記錄,且 需要 發文權限 tran_post_auto_recorded_and_require_post_permissions = ('轉錄文章 會 自動記錄,且 需要 發文權限' in ori_screen) logger.debug('轉錄文章 會 自動記錄,且 需要 發文權限', tran_post_auto_recorded_and_require_post_permissions) cool_mode = ('未 設為冷靜模式' not in ori_screen) logger.debug('冷靜模式', cool_mode) require18 = ('禁止 未滿十八歲進入' in ori_screen) logger.debug('禁止未滿十八歲進入', require18) p = re.compile('登入次數 [\d]+ 次以上') r = p.search(ori_screen) if r is not None: require_login_time = r.group(0).split(' ')[1] require_login_time = int(require_login_time) else: require_login_time = 0 logger.debug('發文限制登入次數', require_login_time) p = re.compile('退文篇數 [\d]+ 篇以下') r = p.search(ori_screen) if r is not None: require_illegal_post = r.group(0).split(' ')[1] require_illegal_post = int(require_illegal_post) else: require_illegal_post = 0 logger.debug('發文限制退文篇數', require_illegal_post) kind_list = [] if get_post_kind: _api_util.goto_board(api, board) # Go certain board, then post to get post type info cmd_list = [] cmd_list.append(command.ctrl_p) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True), connect_core.TargetUnit('或不選)', break_detect=True) ] index = api.connect_core.send( cmd, target_list) if index == 0: raise exceptions.NoPermission(i18n.no_permission) # no post permission ori_screen = api.connect_core.get_screen_queue()[-1] screen_lines = ori_screen.split('\n') for i in screen_lines: if '種類:' in i: type_pattern = re.compile('\d\.([^\ ]*)') # 0 is not present any type that the key hold None object kind_list = type_pattern.findall(i) break # Clear post status cmd_list = [] cmd_list.append(command.ctrl_c) cmd_list.append(command.ctrl_c) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit(screens.Target.InBoard, break_detect=True) ] api.connect_core.send( cmd, target_list) logger.info( i18n.replace(i18n.get_board_info, board), '...', i18n.success ) return { BoardField.board: boardname, BoardField.online_user: online_user, BoardField.mandarin_des: chinese_des, BoardField.moderators: moderators, BoardField.open_status: open_status, BoardField.into_top_ten_when_hide: into_top_ten_when_hide, BoardField.can_non_board_members_post: non_board_members_post, BoardField.can_reply_post: reply_post, BoardField.self_del_post: self_del_post, BoardField.can_comment_post: push_post, BoardField.can_boo_post: boo_post, BoardField.can_fast_push: fast_push, BoardField.min_interval_between_comments: min_interval, BoardField.is_comment_record_ip: push_record_ip, BoardField.is_comment_aligned: push_aligned, BoardField.can_moderators_del_illegal_content: moderator_can_del_illegal_content, BoardField.does_tran_post_auto_recorded_and_require_post_permissions: tran_post_auto_recorded_and_require_post_permissions, BoardField.is_cool_mode: cool_mode, BoardField.is_require18: require18, BoardField.require_login_time: require_login_time, BoardField.require_illegal_post: require_illegal_post, BoardField.post_kind_list: kind_list } ================================================ FILE: PyPtt/_api_get_board_list.py ================================================ import progressbar from . import _api_util from . import command from . import connect_core from . import exceptions from . import i18n from . import log from . import screens def get_board_list(api) -> list: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) cmd_list = [ command.go_main_menu, 'F', command.enter, 'y', '$'] cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit(screens.Target.InBoardList, break_detect=True) ] api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_long_timeout) ori_screen = api.connect_core.get_screen_queue()[-1] max_no = 0 for line in ori_screen.split('\n'): if '◎' not in line and '●' not in line: continue if line.startswith(api.cursor): line = line[len(api.cursor):] # print(f'->{line}<') if '◎' in line: front_part = line[:line.find('◎')] else: front_part = line[:line.find('●')] front_part_list = [x for x in front_part.split(' ')] front_part_list = list(filter(None, front_part_list)) # print(f'FrontPartList =>{FrontPartList}<=') max_no = int(front_part_list[0].rstrip(')')) if api.config.log_level == log.INFO: pb = progressbar.ProgressBar( max_value=max_no, redirect_stdout=True) cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append('F') cmd_list.append(command.enter) cmd_list.append('y') cmd_list.append('0') cmd = ''.join(cmd_list) board_list = [] while True: api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_long_timeout) ori_screen = api.connect_core.get_screen_queue()[-1] # print(OriScreen) for line in ori_screen.split('\n'): if '◎' not in line and '●' not in line: continue if line.startswith(api.cursor): line = line[len(api.cursor):] if '◎' in line: front_part = line[:line.find('◎')] else: front_part = line[:line.find('●')] front_part_list = [x for x in front_part.split(' ')] front_part_list = list(filter(None, front_part_list)) number = front_part_list[0] if ')' in number: number = number[:number.rfind(')')] no = int(number) board_name = front_part_list[1] if board_name.startswith('ˇ'): board_name = board_name[1:] if len(board_name) == 0: board_name = front_part_list[2] board_list.append(board_name) if api.config.log_level == log.INFO: pb.update(no) if no >= max_no: break cmd = command.ctrl_f if api.config.log_level == log.INFO: pb.finish() return board_list ================================================ FILE: PyPtt/_api_get_bottom_post_list.py ================================================ from . import _api_util from . import check_value from . import command from . import connect_core from . import data_type from . import exceptions from . import i18n from . import log from . import screens def get_bottom_post_list(api, board): _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) check_value.check_type(board, str, 'board') log.logger.info(i18n.catch_bottom_post) _api_util.check_board(api, board) _api_util.goto_board(api, board, end=True) last_screen = api.connect_core.get_screen_queue()[-1] bottom_screen = [line for line in last_screen.split('\n') if '★' in line[:8]] bottom_length = len(bottom_screen) if bottom_length == 0: log.logger.info(i18n.catch_bottom_post_success) return list() cmd_list = [] cmd_list.append(command.query_post) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit( screens.Target.PTT1_QueryPost if api.config.host == data_type.HOST.PTT1 else screens.Target.PTT2_QueryPost, log_level=log.DEBUG, break_detect=True, refresh=False), connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)), ] aid_list = [] result = [] for _ in range(bottom_length): api.connect_core.send(cmd, target_list) last_screen = api.connect_core.get_screen_queue()[-1] lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index = \ _api_util.parse_query_post( api, last_screen) aid_list.append(post_aid) cmd_list = [] cmd_list.append(command.enter) cmd_list.append(command.up) cmd_list.append(command.query_post) cmd = ''.join(cmd_list) aid_list.reverse() for post_aid in aid_list: current_post = api.get_post(board=board, aid=post_aid, query=True) result.append(current_post) log.logger.info(i18n.catch_bottom_post, '...', i18n.success) return list(reversed(result)) ================================================ FILE: PyPtt/_api_get_favourite_board.py ================================================ from . import _api_util from . import command from . import connect_core from . import exceptions from . import i18n from . import log from .data_type import FavouriteBoardField def get_favourite_board(api) -> list: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) cmd_list = [command.go_main_menu, 'F', command.enter, '0'] cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('選擇看板', break_detect=True) ] log.logger.info(i18n.get_favourite_board_list) board_list = [] favourite_board_list = [] while True: api.connect_core.send( cmd, target_list) ori_screen = api.connect_core.get_screen_queue()[-1] # print(OriScreen) screen_buf = ori_screen screen_buf = [x for x in screen_buf.split('\n')][3:-1] # adjust for cursor screen_buf[0] = ' ' + screen_buf[0][1:] screen_buf = [x for x in screen_buf] min_len = 47 for i, line in enumerate(screen_buf): if len(screen_buf[i]) == 0: continue if len(screen_buf[i]) <= min_len: # print(f'[{ScreenBuf[i]}]') screen_buf[i] = screen_buf[i] + (' ' * ((min_len + 1) - len(screen_buf[i]))) screen_buf = [x[10:min_len - len(x)].strip() for x in screen_buf] screen_buf = list(filter(None, screen_buf)) for i, line in enumerate(screen_buf): if '------------' in line: continue temp = line.strip().split(' ') no_space_temp = list(filter(None, temp)) board = no_space_temp[0] if board.startswith('ˇ'): board = board[1:] board_type = no_space_temp[1] title_start_index = temp.index(board_type) + 1 board_title = ' '.join(temp[title_start_index:]) # remove ◎ board_title = board_title[1:] if board in board_list: log.logger.info(i18n.success) return favourite_board_list board_list.append(board) favourite_board_list.append({ FavouriteBoardField.board: board, FavouriteBoardField.type: board_type, FavouriteBoardField.title: board_title}) if len(screen_buf) < 20: break cmd = command.ctrl_f log.logger.info(i18n.get_favourite_board_list, '...', i18n.success) return favourite_board_list ================================================ FILE: PyPtt/_api_get_newest_index.py ================================================ from __future__ import annotations import re from typing import Optional from . import _api_util from . import check_value from . import command from . import connect_core from . import data_type, lib_util from . import exceptions from . import i18n from . import log from . import screens def _get_newest_index(api) -> int: last_screen = api.connect_core.get_screen_queue()[-1] last_screen_list = last_screen.split('\n') last_screen_list = last_screen_list[3:] last_screen_list = '\n'.join([x[:9] for x in last_screen_list]) # print(last_screen_list) all_index = re.findall(r'\d+', last_screen_list) if len(all_index) == 0: return 0 all_index = list(map(int, all_index)) all_index.sort(reverse=True) # print(all_index) max_check_range = 6 newest_index = 0 for index_temp in all_index: need_continue = True if index_temp > max_check_range: check_range = max_check_range else: check_range = index_temp for i in range(1, check_range): if str(index_temp - i) not in last_screen: need_continue = False break if need_continue: log.logger.debug(i18n.find_newest_index, index_temp) newest_index = index_temp break if newest_index == 0: raise exceptions.UnknownError('UnknownError') return newest_index def get_newest_index(api, index_type: data_type.NewIndex, board: Optional[str] = None, search_type: data_type.SearchType = None, search_condition: Optional[str] = None, search_list: Optional[list] = None) -> int: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if search_list is None: search_list = [] else: check_value.check_type(search_list, list, 'search_list') if (search_type, search_condition) != (None, None): search_list.insert(0, (search_type, search_condition)) for search_type, search_condition in search_list: check_value.check_type(search_type, data_type.SearchType, 'search_type') check_value.check_type(search_condition, str, 'search_condition') check_value.check_type(index_type, data_type.NewIndex, 'index_type') data_key = f'{index_type}_{board}_{search_list}' if data_key in api._newest_index_data: return api._newest_index_data[data_key] if index_type == data_type.NewIndex.BOARD: check_value.check_type(board, str, 'board') _api_util.check_board(api, board) _api_util.goto_board(api, board) cmd_list = [] cmd_list.append('1') cmd_list.append(command.enter) cmd_list.append('$') cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('沒有文章...', log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)), ] index = api.connect_core.send(cmd, target_list) if index < 0: raise exceptions.NoSuchBoard(api.config, board) if index == 0: return 0 normal_newest_index = _get_newest_index(api) if search_list is not None and len(search_list) > 0: target_list.insert(2, connect_core.TargetUnit( screens.Target.InBoardWithCursor, log_level=log.DEBUG, break_detect=True)) cmd_list = _api_util.get_search_condition_cmd(index_type, search_list) cmd_list.append('1') cmd_list.append(command.enter) cmd_list.append('$') cmd = ''.join(cmd_list) index = api.connect_core.send(cmd, target_list) if index < 0: raise exceptions.NoSuchBoard(api.config, board) if index == 0: return 0 newest_index = _get_newest_index(api) if normal_newest_index == newest_index: raise exceptions.NoSearchResult() else: newest_index = normal_newest_index elif index_type == data_type.NewIndex.MAIL: if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) if board is not None: raise ValueError('board should not input at NewIndex.MAIL.') cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append(command.ctrl_z) cmd_list.append('m') cmd_list.append('1') cmd_list.append(command.enter) cmd_list.append('$') cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit(screens.Target.InMailBox, break_detect=True), connect_core.TargetUnit(screens.Target.CursorToGoodbye, response=cmd), ] def get_index(api): current_capacity, _ = _api_util.get_mailbox_capacity(api) last_screen = api.connect_core.get_screen_queue()[-1] cursor_line = [x for x in last_screen.split('\n') if x.strip().startswith(api.cursor)][0] list_index = int(re.compile('(\d+)').search(cursor_line).group(0)) if search_type == 0 and search_list is None: if list_index > current_capacity: newest_index = list_index else: newest_index = current_capacity else: newest_index = list_index return newest_index newest_index = 0 index = api.connect_core.send( cmd, target_list) if index == 0: normal_newest_index = get_index(api) if search_list is not None and len(search_list) > 0: target_list.insert( 2, connect_core.TargetUnit( screens.Target.InMailBoxWithCursor, log_level=log.DEBUG, break_detect=True) ) cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append(command.ctrl_z) cmd_list.append('m') cmd_list.extend( _api_util.get_search_condition_cmd(index_type, search_list) ) cmd_list.append('1') cmd_list.append(command.enter) cmd_list.append('$') cmd = ''.join(cmd_list) index = api.connect_core.send( cmd, target_list) if index in [0, 2]: newest_index = get_index(api) if normal_newest_index == newest_index: raise exceptions.NoSearchResult() else: newest_index = normal_newest_index api._newest_index_data[data_key] = newest_index return newest_index ================================================ FILE: PyPtt/_api_get_post.py ================================================ from __future__ import annotations import json import re import time from typing import Dict, Optional from AutoStrEnum import AutoJsonEncoder from . import _api_util from . import check_value from . import command from . import connect_core from . import data_type from . import exceptions from . import i18n from . import lib_util from . import log from . import screens from .data_type import PostField, CommentField def get_post(api, board: str, aid: Optional[str] = None, index: Optional[int] = None, search_list: Optional[list] = None, search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None, query: bool = False) -> Dict: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) check_value.check_type(board, str, 'board') if aid is not None: check_value.check_type(aid, str, 'aid') if index is not None: check_value.check_type(index, int, 'index') if search_list is None: search_list = [] else: check_value.check_type(search_list, list, 'search_list') if (search_type, search_condition) != (None, None): search_list.insert(0, (search_type, search_condition)) for search_type, search_condition in search_list: check_value.check_type(search_type, data_type.SearchType, 'search_type') check_value.check_type(search_condition, str, 'search_condition') if len(board) == 0: raise ValueError(f'board error parameter: {board}') if index is not None and isinstance(aid, str): raise ValueError('wrong parameter index and aid can\'t both input') if index is None and aid is None: raise ValueError('wrong parameter index or aid must input') search_cmd = None if search_list is not None and len(search_list) > 0: current_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board, search_list=search_list) search_cmd = _api_util.get_search_condition_cmd(data_type.NewIndex.BOARD, search_list) else: current_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board) if index is not None: check_value.check_index('index', index, current_index) max_retry = 2 post = {} for i in range(max_retry): try: post = _get_post(api, board, aid, index, query, search_cmd) if not post: pass elif not post[PostField.pass_format_check]: pass else: break except exceptions.UnknownError: if i == max_retry - 1: raise except exceptions.NoSuchBoard: if i == max_retry - 1: raise log.logger.debug('Wait for retry repost') time.sleep(0.1) post = json.dumps(post, cls=AutoJsonEncoder) return json.loads(post) def _get_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0, query: bool = False, search_cmd_list: Optional[list[str]] = None) -> Dict: _api_util.check_board(api, board) _api_util.goto_board(api, board) cmd_list = [] if post_aid is not None: cmd_list.append(lib_util.check_aid(post_aid)) elif post_index != 0: if search_cmd_list is not None: cmd_list.extend(search_cmd_list) cmd_list.append(str(max(1, post_index - 100))) cmd_list.append(command.enter) cmd_list.append(str(post_index)) else: raise ValueError('post_aid and post_index cannot be None at the same time') cmd_list.append(command.enter) cmd_list.append(command.query_post) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit( screens.Target.PTT1_QueryPost if api.config.host == data_type.HOST.PTT1 else screens.Target.PTT2_QueryPost, log_level=log.DEBUG, break_detect=True, refresh=False), connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)), ] index = api.connect_core.send(cmd, target_list) ori_screen = api.connect_core.get_screen_queue()[-1] post = { PostField.board: None, PostField.aid: None, PostField.index: None, PostField.author: None, PostField.date: None, PostField.title: None, PostField.content: None, PostField.money: None, PostField.url: None, PostField.ip: None, PostField.comments: [], PostField.post_status: data_type.PostStatus.EXISTS, PostField.list_date: None, PostField.has_control_code: False, PostField.pass_format_check: False, PostField.location: None, PostField.push_number: None, PostField.is_lock: False, PostField.full_content: None, PostField.is_unconfirmed: False} post_author = None post_title = None if index < 0 or index == 1: # 文章被刪除 log.logger.debug(i18n.post_deleted) log.logger.debug('OriScreen', ori_screen) cursor_line = [line for line in ori_screen.split( '\n') if line.startswith(api.cursor)] if len(cursor_line) != 1: raise exceptions.UnknownError(ori_screen) cursor_line = cursor_line[0] log.logger.debug('CursorLine', cursor_line) pattern = re.compile('[\d]+\/[\d]+') pattern_result = pattern.search(cursor_line) if pattern_result is None: list_date = None else: list_date = pattern_result.group(0) list_date = list_date[-5:] pattern = re.compile('\[[\w]+\]') pattern_result = pattern.search(cursor_line) if pattern_result is not None: post_del_status = data_type.PostStatus.DELETED_BY_AUTHOR else: pattern = re.compile('<[\w]+>') pattern_result = pattern.search(cursor_line) post_del_status = data_type.PostStatus.DELETED_BY_MODERATOR # > 79843 9/11 - □ (本文已被吃掉)< # > 76060 8/28 - □ (本文已被刪除) [weida7332] # print(f'O=>{CursorLine}<') if pattern_result is not None: post_author = pattern_result.group(0)[1:-1] else: post_author = None post_del_status = data_type.PostStatus.DELETED_BY_UNKNOWN log.logger.debug('ListDate', list_date) log.logger.debug('PostAuthor', post_author) log.logger.debug('post_del_status', post_del_status) post.update({ PostField.board: board, PostField.author: post_author, PostField.list_date: list_date, PostField.post_status: post_del_status, PostField.pass_format_check: True }) return post elif index == 0: lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index = \ _api_util.parse_query_post( api, ori_screen) if lock_post: post.update({ PostField.board: board, PostField.aid: post_aid, PostField.index: post_index, PostField.author: post_author, PostField.title: post_title, PostField.url: post_web, PostField.money: post_money, PostField.list_date: list_date, PostField.pass_format_check: True, PostField.push_number: push_number, PostField.is_lock: True}) return post if query: post.update({ PostField.board: board, PostField.aid: post_aid, PostField.index: post_index, PostField.author: post_author, PostField.title: post_title, PostField.url: post_web, PostField.money: post_money, PostField.list_date: list_date, PostField.pass_format_check: True, PostField.push_number: push_number}) return post origin_post, has_control_code = _api_util.get_content(api) if origin_post is None: log.logger.info(i18n.post_deleted) post.update({ PostField.board: board, PostField.aid: post_aid, PostField.index: post_index, PostField.author: post_author, PostField.title: post_title, PostField.url: post_web, PostField.money: post_money, PostField.list_date: list_date, PostField.has_control_code: has_control_code, PostField.pass_format_check: False, PostField.push_number: push_number, PostField.is_unconfirmed: api.Unconfirmed }) return post post_author_pattern_new = re.compile('作者 (.+) 看板') post_author_pattern_old = re.compile('作者 (.+)') board_pattern = re.compile('看板 (.+)') post_date = None post_content = None ip = None location = None push_list = [] # 格式確認,亂改的我也沒辦法Q_Q origin_post_lines = origin_post.split('\n') author_line = origin_post_lines[0] if board.lower() == 'allpost': board_line = author_line[author_line.find(')') + 1:] pattern_result = board_pattern.search(board_line) if pattern_result is not None: board_temp = post_author = pattern_result.group(0) board_temp = board_temp[2:].strip() if len(board_temp) > 0: board = board_temp log.logger.debug(i18n.board, board) pattern_result = post_author_pattern_new.search(author_line) if pattern_result is not None: post_author = pattern_result.group(0) post_author = post_author[:post_author.rfind(')') + 1] else: pattern_result = post_author_pattern_old.search(author_line) if pattern_result is None: log.logger.info(i18n.substandard_post, i18n.author) post.update({ PostField.board: board, PostField.aid: post_aid, PostField.index: post_index, PostField.author: post_author, PostField.date: post_date, PostField.title: post_title, PostField.url: post_web, PostField.money: post_money, PostField.content: post_content, PostField.ip: ip, PostField.comments: push_list, PostField.list_date: list_date, PostField.has_control_code: has_control_code, PostField.pass_format_check: False, PostField.location: location, PostField.push_number: push_number, PostField.full_content: origin_post, PostField.is_unconfirmed: api.Unconfirmed, }) return post post_author = pattern_result.group(0) post_author = post_author[:post_author.rfind(')') + 1] post_author = post_author[4:].strip() log.logger.debug(i18n.author, post_author) post_title_pattern = re.compile('標題 (.+)') title_line = origin_post_lines[1] pattern_result = post_title_pattern.search(title_line) if pattern_result is None: log.logger.info(i18n.substandard_post, i18n.title) post.update({ PostField.board: board, PostField.aid: post_aid, PostField.index: post_index, PostField.author: post_author, PostField.date: post_date, PostField.title: post_title, PostField.url: post_web, PostField.money: post_money, PostField.content: post_content, PostField.ip: ip, PostField.comments: push_list, PostField.list_date: list_date, PostField.has_control_code: has_control_code, PostField.pass_format_check: False, PostField.location: location, PostField.push_number: push_number, PostField.full_content: origin_post, PostField.is_unconfirmed: api.Unconfirmed, }) return post post_title = pattern_result.group(0) post_title = post_title[4:].strip() log.logger.debug(i18n.title, post_title) post_date_pattern = re.compile('時間 .{24}') date_line = origin_post_lines[2] pattern_result = post_date_pattern.search(date_line) if pattern_result is None: log.logger.info(i18n.substandard_post, i18n.date) post.update({ PostField.board: board, PostField.aid: post_aid, PostField.index: post_index, PostField.author: post_author, PostField.date: post_date, PostField.title: post_title, PostField.url: post_web, PostField.money: post_money, PostField.content: post_content, PostField.ip: ip, PostField.comments: push_list, PostField.list_date: list_date, PostField.has_control_code: has_control_code, PostField.pass_format_check: False, PostField.location: location, PostField.push_number: push_number, PostField.full_content: origin_post, PostField.is_unconfirmed: api.Unconfirmed, }) return post post_date = pattern_result.group(0) post_date = post_date[4:].strip() log.logger.debug(i18n.date, post_date) content_fail = True if screens.Target.content_start not in origin_post: # print('Type 1') content_fail = True else: post_content = origin_post post_content = post_content[ post_content.find(screens.Target.content_start) + len(screens.Target.content_start) + 1:] # print('Type 2') # print(f'PostContent [{PostContent}]') for content_end in screens.Target.content_end_list: # + 3 = 把 --\n 拿掉 # print(f'EC [{EC}]') if content_end in post_content: content_fail = False post_content = post_content[:post_content.rfind(content_end) + 3] origin_post_lines = origin_post[origin_post.find(content_end):] # post_content = post_content.strip() origin_post_lines = origin_post_lines.split('\n') break if content_fail: log.logger.info(i18n.substandard_post, i18n.content) post.update({ PostField.board: board, PostField.aid: post_aid, PostField.index: post_index, PostField.author: post_author, PostField.date: post_date, PostField.title: post_title, PostField.url: post_web, PostField.money: post_money, PostField.content: post_content, PostField.ip: ip, PostField.comments: push_list, PostField.list_date: list_date, PostField.has_control_code: has_control_code, PostField.pass_format_check: False, PostField.location: location, PostField.push_number: push_number, PostField.full_content: origin_post, PostField.is_unconfirmed: api.Unconfirmed, }) return post log.logger.debug(i18n.content, post_content) info_lines = [line for line in origin_post_lines if line.startswith('※') or line.startswith('◆')] pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+') pattern_p2 = re.compile('[\d]+-[\d]+-[\d]+-[\d]+') for line in reversed(info_lines): log.logger.debug('IP Line', line) # type 1 # ※ 編輯: CodingMan (111.243.146.98 臺灣) # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 111.243.146.98 (臺灣) # type 2 # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 116.241.32.178 # ※ 編輯: kill77845 (114.136.55.237), 12/08/2018 16:47:59 # type 3 # ※ 發信站: 批踢踢實業坊(ptt.cc) # ◆ From: 211.20.78.69 # ※ 編輯: JCC 來自: 211.20.78.69 (06/20 10:22) # ※ 編輯: JCC (118.163.28.150), 12/03/2015 14:25:35 pattern_result = pattern.search(line) if pattern_result is not None: ip = pattern_result.group(0) location_temp = line[line.find(ip) + len(ip):].strip() location_temp = location_temp.replace('(', '') location_temp = location_temp[:location_temp.rfind(')')] location_temp = location_temp.strip() # print(f'=>[{LocationTemp}]') if ' ' not in location_temp and len(location_temp) > 0: location = location_temp log.logger.debug('Location', location) break pattern_result = pattern_p2.search(line) if pattern_result is not None: ip = pattern_result.group(0) ip = ip.replace('-', '.') # print(f'IP -> [{IP}]') break if api.config.host == data_type.HOST.PTT1: if ip is None: log.logger.info(i18n.substandard_post, ip) post.update({ PostField.board: board, PostField.aid: post_aid, PostField.index: post_index, PostField.author: post_author, PostField.date: post_date, PostField.title: post_title, PostField.url: post_web, PostField.money: post_money, PostField.content: post_content, PostField.ip: ip, PostField.comments: push_list, PostField.list_date: list_date, PostField.has_control_code: has_control_code, PostField.pass_format_check: False, PostField.location: location, PostField.push_number: push_number, PostField.full_content: origin_post, PostField.is_unconfirmed: api.Unconfirmed, }) return post log.logger.debug('IP', ip) push_author_pattern = re.compile('[推|噓|→] [\w| ]+:') push_date_pattern = re.compile('[\d]+/[\d]+ [\d]+:[\d]+') push_ip_pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+') push_list = [] for line in origin_post_lines: if line.startswith('推'): comment_type = data_type.CommentType.PUSH elif line.startswith('噓 '): comment_type = data_type.CommentType.BOO elif line.startswith('→ '): comment_type = data_type.CommentType.ARROW else: continue result = push_author_pattern.search(line) if result is None: # 不符合推文格式 continue push_author = result.group(0)[2:-1].strip() log.logger.debug(i18n.comment_id, push_author) result = push_date_pattern.search(line) if result is None: continue push_date = result.group(0) log.logger.debug(i18n.comment_date, push_date) comment_ip = None result = push_ip_pattern.search(line) if result is not None: comment_ip = result.group(0) log.logger.debug(f'{i18n.comment} ip', comment_ip) push_content = line[line.find(push_author) + len(push_author):] # PushContent = PushContent.replace(PushDate, '') if api.config.host == data_type.HOST.PTT1: push_content = push_content[:push_content.rfind(push_date)] else: # → CodingMan:What is Ptt? 推 10/04 13:25 push_content = push_content[:push_content.rfind(push_date) - 2] if comment_ip is not None: push_content = push_content.replace(comment_ip, '') push_content = push_content[push_content.find(':') + 1:].strip() log.logger.debug(i18n.comment_content, push_content) current_push = { CommentField.type: comment_type, CommentField.author: push_author, CommentField.content: push_content, CommentField.ip: comment_ip, CommentField.time: push_date} push_list.append(current_push) post.update({ PostField.board: board, PostField.aid: post_aid, PostField.index: post_index, PostField.author: post_author, PostField.date: post_date, PostField.title: post_title, PostField.url: post_web, PostField.money: post_money, PostField.content: post_content, PostField.ip: ip, PostField.comments: push_list, PostField.list_date: list_date, PostField.has_control_code: has_control_code, PostField.pass_format_check: True, PostField.location: location, PostField.push_number: push_number, PostField.full_content: origin_post, PostField.is_unconfirmed: api.Unconfirmed}) return post ================================================ FILE: PyPtt/_api_get_post_index.py ================================================ from . import _api_util from . import command from . import connect_core from . import exceptions from . import i18n from . import log from . import screens def get_post_index(api, board: str, aid: str) -> int: _api_util.goto_board(api, board) cmd_list = [] cmd_list.append('#') cmd_list.append(aid) cmd_list.append(command.enter) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('找不到這個文章代碼', log_level=log.DEBUG, exceptions_=exceptions.NoSuchPost(board, aid)), # 此狀態下無法使用搜尋文章代碼(AID)功能 connect_core.TargetUnit('此狀態下無法使用搜尋文章代碼(AID)功能', exceptions_=exceptions.CanNotUseSearchPostCode()), connect_core.TargetUnit('沒有文章...', exceptions_=exceptions.NoSuchPost(board, aid)), connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit(screens.Target.InBoardWithCursor, log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)) ] index = api.connect_core.send( cmd, target_list ) ori_screen = api.connect_core.get_screen_queue()[-1] if index < 0: # print(OriScreen) raise exceptions.NoSuchBoard(api.config, board) # if index == 5: # print(OriScreen) # raise exceptions.NoSuchBoard(api.config, board) # print(index) # print(OriScreen) screen_list = ori_screen.split('\n') line = [x for x in screen_list if x.startswith(api.cursor)] line = line[0] last_line = screen_list[screen_list.index(line) - 1] # print(LastLine) # print(line) if '編號' in last_line and '人氣:' in last_line: index = line[1:].strip() index_fix = False else: index = last_line.strip() index_fix = True while ' ' in index: index = index.replace(' ', ' ') index_list = index.split(' ') index = index_list[0] if index == '★': return 0 index = int(index) if index_fix: index += 1 # print(Index) return index ================================================ FILE: PyPtt/_api_get_time.py ================================================ import re from . import _api_util from . import command from . import connect_core from . import exceptions from . import i18n from . import log from . import screens pattern = re.compile('[\d]+:[\d][\d]') def get_time(api) -> str: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append('A') cmd_list.append(command.right) cmd_list.append(command.left) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True), ] index = api.connect_core.send(cmd, target_list) if index != 0: return None ori_screen = api.connect_core.get_screen_queue()[-1] line_list = ori_screen.split('\n')[-3:] # 0:00 for line in line_list: if '星期' in line and '線上' in line and '我是' in line: result = pattern.search(line) if result is not None: return result.group(0) return None ================================================ FILE: PyPtt/_api_get_user.py ================================================ import json import re from typing import Dict from AutoStrEnum import AutoJsonEncoder from . import _api_util from . import check_value from . import command from . import connect_core from . import data_type from . import exceptions from . import i18n from . import lib_util from . import log from . import screens from .data_type import UserField def get_user(api, ptt_id: str) -> Dict: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) check_value.check_type(ptt_id, str, 'UserID') if len(ptt_id) < 2: raise ValueError(f'wrong parameter user_id: {ptt_id}') cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append('T') cmd_list.append(command.enter) cmd_list.append('Q') cmd_list.append(command.enter) cmd_list.append(ptt_id) cmd_list.append(command.enter) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit(screens.Target.AnyKey, break_detect=True), connect_core.TargetUnit(screens.Target.InTalk, break_detect=True), ] index = api.connect_core.send( cmd, target_list) ori_screen = api.connect_core.get_screen_queue()[-1] if index == 1: raise exceptions.NoSuchUser(ptt_id) # PTT1 # 《ID暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》小康 ($73866) # 《登入次數》1118 次 (同天內只計一次) 《有效文章》15 篇 (退:0) # 《目前動態》閱讀文章 《私人信箱》最近無新信件 # 《上次上站》10/06/2019 17:29:49 Sun 《上次故鄉》111.251.231.184 # 《 五子棋 》 0 勝 0 敗 0 和 《象棋戰績》 0 勝 0 敗 0 和 # https://github.com/Truth0906/PTTLibrary # 強大的 PTT 函式庫 # 提供您 快速 穩定 完整 的 PTT API # 提供專業的 PTT 機器人諮詢服務 # PTT2 # 《ID暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》家徒四壁 ($0) # 《登入次數》8 次 (同天內只計一次) 《有效文章》0 篇 # 《目前動態》看板列表 《私人信箱》最近無新信件 # 《上次上站》10/06/2019 17:27:55 Sun 《上次故鄉》111.251.231.184 # 《 五子棋 》 0 勝 0 敗 0 和 《象棋戰績》 0 勝 0 敗 0 和 # 《個人名片》CodingMan 目前沒有名片 lines = ori_screen.split('\n')[1:] def parse_user_info_from_line(line: str) -> (str, str): part_0 = line[line.find('》') + 1:] part_0 = part_0[:part_0.find('《')].strip() part_1 = line[line.rfind('》') + 1:].strip() return part_0, part_1 ptt_id, buff_1 = parse_user_info_from_line(lines[0]) money = int(int_list[0]) if len(int_list := re.findall(r'\d+', buff_1)) > 0 else buff_1 buff_0, buff_1 = parse_user_info_from_line(lines[1]) login_count = int(re.findall(r'\d+', buff_0)[0]) account_verified = ('同天內只計一次' in buff_0) legal_post = int(re.findall(r'\d+', buff_1)[0]) # PTT2 沒有退文 if api.config.host == data_type.HOST.PTT1: illegal_post = int(re.findall(r'\d+', buff_1)[1]) else: illegal_post = None activity, mail = parse_user_info_from_line(lines[2]) last_login_date, last_login_ip = parse_user_info_from_line(lines[3]) five_chess, chess = parse_user_info_from_line(lines[4]) signature_file = '\n'.join(lines[5:-1]).strip('\n') log.logger.debug('ptt_id', ptt_id) log.logger.debug('money', money) log.logger.debug('login_count', login_count) log.logger.debug('account_verified', account_verified) log.logger.debug('legal_post', legal_post) log.logger.debug('illegal_post', illegal_post) log.logger.debug('activity', activity) log.logger.debug('mail', mail) log.logger.debug('last_login_date', last_login_date) log.logger.debug('last_login_ip', last_login_ip) log.logger.debug('five_chess', five_chess) log.logger.debug('chess', chess) log.logger.debug('signature_file', signature_file) user = { UserField.ptt_id: ptt_id, UserField.money: money, UserField.login_count: login_count, UserField.account_verified: account_verified, UserField.legal_post: legal_post, UserField.illegal_post: illegal_post, UserField.activity: activity, UserField.mail: mail, UserField.last_login_date: last_login_date, UserField.last_login_ip: last_login_ip, UserField.five_chess: five_chess, UserField.chess: chess, UserField.signature_file: signature_file, } user = json.dumps(user, cls=AutoJsonEncoder) return json.loads(user) ================================================ FILE: PyPtt/_api_give_money.py ================================================ from . import _api_util from . import check_value from . import command from . import connect_core from . import exceptions from . import i18n from . import lib_util from . import log def give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag_content: str) -> None: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) check_value.check_type(ptt_id, str, 'ptt_id') check_value.check_type(money, int, 'money') if red_bag_title is not None: check_value.check_type(red_bag_title, str, 'red_bag_title') else: red_bag_title = '' if red_bag_content is not None: check_value.check_type(red_bag_content, str, 'red_bag_content') else: red_bag_content = '' log.logger.info( i18n.replace(i18n.give_money_to, ptt_id, money)) # Check data_type.user api.get_user(ptt_id) cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append('P') cmd_list.append(command.enter) cmd_list.append('P') cmd_list.append(command.enter) cmd_list.append('O') cmd_list.append(command.enter) cmd = ''.join(cmd_list) edit_red_bag_cmd_list = list() edit_red_bag_target = connect_core.TargetUnit('要修改紅包袋嗎', response='n' + command.enter) if red_bag_title != '' or red_bag_content != '': edit_red_bag_cmd_list.append('y') edit_red_bag_cmd_list.append(command.enter) if red_bag_title != '': edit_red_bag_cmd_list.append(command.down) edit_red_bag_cmd_list.append(command.ctrl_y) # remove the red_bag_title edit_red_bag_cmd_list.append(command.enter) edit_red_bag_cmd_list.append(command.up) edit_red_bag_cmd_list.append(f'標題: {red_bag_title}') # reset cursor to original position edit_red_bag_cmd_list.append(command.up * 2) if red_bag_content != '': edit_red_bag_cmd_list.append(command.down * 4) edit_red_bag_cmd_list.append(command.ctrl_y * 8) # remove original red_bag_content edit_red_bag_cmd_list.append(red_bag_content) edit_red_bag_cmd_list.append(command.ctrl_x) edit_red_bag_cmd = ''.join(edit_red_bag_cmd_list) edit_red_bag_target = connect_core.TargetUnit('要修改紅包袋嗎', response=edit_red_bag_cmd) target_list = [ connect_core.TargetUnit('你沒有那麼多Ptt幣喔!', break_detect=True, exceptions_=exceptions.NoMoney()), connect_core.TargetUnit('金額過少,交易取消!', break_detect=True, exceptions_=exceptions.NoMoney()), connect_core.TargetUnit('交易取消!', break_detect=True, exceptions_=exceptions.UnknownError(i18n.transaction_cancelled)), connect_core.TargetUnit('確定進行交易嗎?', response='y' + command.enter), connect_core.TargetUnit('按任意鍵繼續', break_detect=True), edit_red_bag_target, connect_core.TargetUnit('要修改紅包袋嗎', response=command.enter), connect_core.TargetUnit('完成交易前要重新確認您的身份', response=api._ptt_pw + command.enter), connect_core.TargetUnit('他是你的小主人,是否匿名?', response='n' + command.enter), connect_core.TargetUnit('要給他多少Ptt幣呢?', response=command.tab + str(money) + command.enter), connect_core.TargetUnit('這位幸運兒的id', response=ptt_id + command.enter), connect_core.TargetUnit('認證尚未過期', response='y' + command.enter), connect_core.TargetUnit('交易正在進行中', response=command.space) ] api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_long_timeout ) log.logger.info( i18n.replace(i18n.give_money_to, ptt_id, money), '...', i18n.success) ================================================ FILE: PyPtt/_api_has_new_mail.py ================================================ import re from . import _api_util from . import command from . import connect_core from . import log from . import screens def has_new_mail(api) -> int: cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append(command.ctrl_z) cmd_list.append('m') # cmd_list.append('1') # cmd_list.append(command.enter) cmd = ''.join(cmd_list) current_capacity = None plus_count = 0 index_pattern = re.compile('(\d+)') checked_index_list = [] break_detect = False target_list = [ connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True) ] api.connect_core.send( cmd, target_list, ) current_capacity, _ = _api_util.get_mailbox_capacity(api) if current_capacity > 20: cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append(command.ctrl_z) cmd_list.append('m') cmd_list.append('1') cmd_list.append(command.enter) cmd = ''.join(cmd_list) while True: if current_capacity > 20: api.connect_core.send( cmd, target_list, ) last_screen = api.connect_core.get_screen_queue()[-1] last_screen_list = last_screen.split('\n') last_screen_list = last_screen_list[3:-1] last_screen_list = [x[:10] for x in last_screen_list] current_plus_count = 0 for line in last_screen_list: if str(current_capacity) in line: break_detect = True index_result = index_pattern.search(line) if index_result is None: continue current_index = index_result.group(0) if current_index in checked_index_list: continue checked_index_list.append(current_index) if '+' not in line: continue current_plus_count += 1 plus_count += current_plus_count if break_detect: break cmd = command.ctrl_f return plus_count ================================================ FILE: PyPtt/_api_loginout.py ================================================ import re from . import _api_util from . import check_value from . import command from . import connect_core from . import data_type from . import exceptions from . import i18n from . import log from . import screens def logout(api) -> None: _api_util.one_thread(api) log.logger.info(i18n.logout) if not api._is_login: log.logger.info(i18n.logout, '...', i18n.success) return cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append('g') cmd_list.append(command.enter) cmd_list.append('y') cmd_list.append(command.enter) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('任意鍵', break_detect=True), ] try: api.connect_core.send(cmd, target_list) api.connect_core.close() except exceptions.ConnectionClosed: pass except RuntimeError: pass api._is_login = False log.logger.info(i18n.logout, '...', i18n.success) def login(api, ptt_id: str, ptt_pw: str, kick_other_session: bool): _api_util.one_thread(api) check_value.check_type(ptt_id, str, 'ptt_id') check_value.check_type(ptt_pw, str, 'password') check_value.check_type(kick_other_session, bool, 'kick_other_session') if api._is_login: api.logout() api.config.kick_other_session = kick_other_session def kick_other_login_display_msg(): if api.config.kick_other_session: return i18n.kick_other_login return i18n.not_kick_other_login def kick_other_login_response(screen): if api.config.kick_other_session: return 'y' + command.enter return 'n' + command.enter api.is_mailbox_full = False # def is_mailbox_full(): # log.log( # api.config, # LogLevel.INFO, # i18n.MailBoxFull) # api.is_mailbox_full = True def register_processing(screen): pattern = re.compile('[\d]+') api.process_picks = int(pattern.search(screen).group(0)) if len(ptt_pw) > 8: ptt_pw = ptt_pw[:8] ptt_id = ptt_id.strip() ptt_pw = ptt_pw.strip() api.ptt_id = ptt_id api._ptt_pw = ptt_pw api.connect_core.connect() log.logger.info(i18n.login_id, ptt_id) target_list = [ connect_core.TargetUnit(screens.Target.InMailBox, response=command.go_main_menu + 'A' + command.right + command.left, break_detect=True), connect_core.TargetUnit(screens.Target.InMailMenu, response=command.go_main_menu), connect_core.TargetUnit(screens.Target.MainMenu, break_detect=True), connect_core.TargetUnit('【看板列表】', response=command.go_main_menu), connect_core.TargetUnit('密碼不對', break_detect=True, exceptions_=exceptions.WrongIDorPassword()), connect_core.TargetUnit('請重新輸入', break_detect=True, exceptions_=exceptions.WrongIDorPassword()), connect_core.TargetUnit('登入太頻繁', response=' '), connect_core.TargetUnit('系統過載', break_detect=True), connect_core.TargetUnit('您要刪除以上錯誤嘗試的記錄嗎', response='y' + command.enter), connect_core.TargetUnit('請選擇暫存檔 (0-9)[0]', response=command.enter), connect_core.TargetUnit('有一篇文章尚未完成', response='Q' + command.enter), connect_core.TargetUnit( '請重新設定您的聯絡信箱', break_detect=True, exceptions_=exceptions.ResetYourContactEmail()), # connect_core.TargetUnit( # i18n.in_login_process_please_wait, # '登入中,請稍候'), connect_core.TargetUnit('密碼正確'), # 密碼正確 connect_core.TargetUnit('您想刪除其他重複登入的連線嗎', response=kick_other_login_response), connect_core.TargetUnit('◆ 您的註冊申請單尚在處理中', response=command.enter, handler=register_processing), connect_core.TargetUnit('任意鍵', response=' '), connect_core.TargetUnit('正在更新與同步線上使用者及好友名單'), connect_core.TargetUnit('【分類看板】', response=command.go_main_menu), connect_core.TargetUnit([ '大富翁', '排行榜', '名次', '代號', '暱稱', '數目' ], response=command.go_main_menu), connect_core.TargetUnit([ '熱門話題' ], response=command.go_main_menu), connect_core.TargetUnit('您確定要填寫註冊單嗎', response=command.enter * 3), connect_core.TargetUnit('以上資料是否正確', response='y' + command.enter), connect_core.TargetUnit('另外若輸入後發生認證碼錯誤請先確認輸入是否為最後一封', response='x' + command.enter), connect_core.TargetUnit('此帳號已設定為只能使用安全連線', exceptions_=exceptions.OnlySecureConnection()) ] # IAC = '\xff' # WILL = '\xfb' # NAWS = '\x1f' cmd_list = [] # cmd_list.append(IAC + WILL + NAWS) cmd_list.append(ptt_id + ',') cmd_list.append(command.enter) cmd_list.append(ptt_pw) cmd_list.append(command.enter) cmd = ''.join(cmd_list) index = api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_long_timeout, refresh=False, secret=True) if index == 0: current_capacity, max_capacity = _api_util.get_mailbox_capacity(api) log.logger.info(i18n.has_new_mail_goto_main_menu) if current_capacity > max_capacity: api.is_mailbox_full = True log.logger.info(i18n.mail_box_full) if api.is_mailbox_full: log.logger.info(i18n.use_mailbox_api_will_logout_after_execution) target_list = [ connect_core.TargetUnit(screens.Target.MainMenu, break_detect=True) ] cmd = command.go_main_menu + 'A' + command.right + command.left index = api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_long_timeout, secret=True) ori_screen = api.connect_core.get_screen_queue()[-1] is_login = True for t in screens.Target.MainMenu: if t not in ori_screen: is_login = False break if not is_login: raise exceptions.LoginError() if '> (' in ori_screen: api.cursor = data_type.Cursor.NEW log.logger.debug(i18n.new_cursor) elif '●(' in ori_screen: api.cursor = data_type.Cursor.OLD log.logger.debug(i18n.old_cursor) else: raise exceptions.UnknownError() screens.Target.InBoardWithCursor = screens.Target.InBoardWithCursor[:screens.Target.InBoardWithCursorLen] screens.Target.InBoardWithCursor.append(api.cursor) screens.Target.InMailBoxWithCursor = screens.Target.InMailBoxWithCursor[:screens.Target.InMailBoxWithCursorLen] screens.Target.InMailBoxWithCursor.append(api.cursor) screens.Target.CursorToGoodbye = screens.Target.CursorToGoodbye[:len(screens.Target.MainMenu)] if api.cursor == '>': screens.Target.CursorToGoodbye.append('> (G)oodbye') else: screens.Target.CursorToGoodbye.append('●(G)oodbye') unregistered_user = True if '(T)alk' in ori_screen: unregistered_user = False if '(P)lay' in ori_screen: unregistered_user = False if '(N)amelist' in ori_screen: unregistered_user = False if unregistered_user: log.logger.info(i18n.unregistered_user_cant_use_all_api) api.is_registered_user = not unregistered_user if api.process_picks != 0: log.logger.info(i18n.picks_in_register, api.process_picks) api._is_login = True log.logger.info(i18n.login_success) ================================================ FILE: PyPtt/_api_mail.py ================================================ from __future__ import annotations import re from typing import Dict, Optional from . import _api_util from . import check_value from . import command from . import connect_core from . import data_type from . import exceptions from . import i18n from . import lib_util from . import log from . import screens from .data_type import MailField # 寄信 def mail(api, ptt_id: str, title: str, content: str, sign_file, backup: bool = True) -> None: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) check_value.check_type(ptt_id, str, 'ptt_id') check_value.check_type(title, str, 'title') check_value.check_type(content, str, 'content') api.get_user(ptt_id) check_sign_file = False for i in range(0, 10): if str(i) == sign_file or i == sign_file: check_sign_file = True break if not check_sign_file: if sign_file.lower() != 'x': raise ValueError(f'wrong parameter sign_file: {sign_file}') cmd_list = [] # 回到主選單 cmd_list.append(command.go_main_menu) # 私人信件區 cmd_list.append('M') cmd_list.append(command.enter) # 站內寄信 cmd_list.append('S') cmd_list.append(command.enter) # 輸入 id cmd_list.append(ptt_id) cmd_list.append(command.enter) cmd = ''.join(cmd_list) # 定義如何根據情況回覆訊息 target_list = [ connect_core.TargetUnit('主題:', break_detect=True), connect_core.TargetUnit('【電子郵件】', exceptions_=exceptions.NoSuchUser(ptt_id)) ] api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_long_timeout ) cmd_list = [] # 輸入標題 cmd_list.append(title) cmd_list.append(command.enter) # 輸入內容 cmd_list.append(content) # 儲存檔案 cmd_list.append(command.ctrl_x) cmd = ''.join(cmd_list) # 定義如何根據情況回覆訊息 target_list = [ connect_core.TargetUnit('請按任意鍵繼續', response=command.enter, break_detect_after_send=True), connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter), connect_core.TargetUnit('是否自存底稿', response=('y' if backup else 'n') + command.enter), connect_core.TargetUnit('選擇簽名檔', response=str(sign_file) + command.enter), connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter), ] # 送出訊息 api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_post_timeout) log.logger.info(i18n.send_mail, i18n.success) # -- # ※ 發信站: 批踢踢實業坊(ptt.cc) # ◆ From: 220.142.14.95 content_start = '───────────────────────────────────────' content_end = '--\n※ 發信站: 批踢踢實業坊(ptt.cc)' content_ip_old = '◆ From: ' mail_author_pattern = re.compile('作者 (.+)') mail_title_pattern = re.compile('標題 (.+)') mail_date_pattern = re.compile('時間 (.+)') ip_pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+') def get_mail(api, index: int, search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None, search_list: Optional[list] = None) -> Dict: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) log.logger.info(i18n.get_mail) if not isinstance(index, int): raise ValueError('index must be int') current_index = api.get_newest_index(data_type.NewIndex.MAIL) if index <= 0 or current_index < index: raise exceptions.NoSuchMail() # check_value.check_index('index', index, current_index) cmd_list = [] # 回到主選單 cmd_list.append(command.go_main_menu) # 進入信箱 cmd_list.append(command.ctrl_z) cmd_list.append('m') # 處理條件整理出指令 _cmd_list = _api_util.get_search_condition_cmd(data_type.NewIndex.MAIL, search_list) cmd_list.extend(_cmd_list) # 前進至目標信件位置 cmd_list.append(str(index)) cmd_list.append(command.enter) cmd = ''.join(cmd_list) # 有時候會沒有最底下一列,只好偵測游標是否出現 if api.cursor == data_type.Cursor.NEW: space_length = 6 - len(api.cursor) - len(str(index)) else: space_length = 5 - len(api.cursor) - len(str(index)) fast_target = f"{api.cursor}{' ' * space_length}{index}" # 定義如何根據情況回覆訊息 target_list = [ connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit(fast_target, log_level=log.DEBUG, break_detect=True) ] # 送出訊息 api.connect_core.send( cmd, target_list) # 取得信件全文 origin_mail, _ = _api_util.get_content(api, post_mode=False) # 使用表示式分析信件作者 pattern_result = mail_author_pattern.search(origin_mail) if pattern_result is None: mail_author = None else: mail_author = pattern_result.group(0)[2:].strip() # 使用表示式分析信件標題 pattern_result = mail_title_pattern.search(origin_mail) if pattern_result is None: mail_title = None else: mail_title = pattern_result.group(0)[2:].strip() # 使用表示式分析信件日期 pattern_result = mail_date_pattern.search(origin_mail) if pattern_result is None: mail_date = None else: mail_date = pattern_result.group(0)[2:].strip() # 從全文拿掉信件開頭作為信件內文 mail_content = origin_mail[origin_mail.find(content_start) + len(content_start) + 1:] mail_content = mail_content[ mail_content.find(screens.Target.content_start) + len(screens.Target.content_start) + 1:] for EC in screens.Target.content_end_list: # + 3 = 把 --\n 拿掉 if EC in mail_content: mail_content = mail_content[:mail_content.rfind(EC) + 3].rstrip() break # 紅包偵測 red_envelope = False if content_end not in origin_mail and 'Ptt幣的大紅包喔' in origin_mail: mail_content = mail_content.strip() red_envelope = True else: mail_content = mail_content[:mail_content.rfind(content_end) + 3] if red_envelope: mail_ip = None mail_location = None else: # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 111.242.182.114 # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 59.104.127.126 (臺灣) # 非紅包開始解析 ip 與 地區 ip_line_list = origin_mail.split('\n') ip_line = [x for x in ip_line_list if x.startswith(content_end[3:])] if len(ip_line) == 0: # 沒 ip 就沒地區 mail_ip = None mail_location = None else: ip_line = ip_line[0] result = ip_pattern.search(ip_line) if result is None: ip_line = [x for x in ip_line_list if x.startswith(content_ip_old)] if len(ip_line) == 0: mail_ip = None else: ip_line = ip_line[0] result = ip_pattern.search(ip_line) mail_ip = result.group(0) else: mail_ip = result.group(0) location = ip_line[ip_line.find(mail_ip) + len(mail_ip):].strip() if len(location) == 0: mail_location = None else: # print(location) mail_location = location[1:-1] log.logger.info(i18n.get_mail, '...', i18n.success) return { MailField.origin_mail: origin_mail, MailField.author: mail_author, MailField.title: mail_title, MailField.date: mail_date, MailField.content: mail_content, MailField.ip: mail_ip, MailField.location: mail_location, MailField.is_red_envelope: red_envelope} def del_mail(api, index) -> None: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) current_index = api.get_newest_index(data_type.NewIndex.MAIL) check_value.check_index(index, current_index) cmd_list = [] # 進入主選單 cmd_list.append(command.go_main_menu) # 進入信箱 cmd_list.append(command.ctrl_z) cmd_list.append('m') if index > 20: # speed up cmd_list.append(str(1)) cmd_list.append(command.enter) # 前進到目標信件位置 cmd_list.append(str(index)) cmd_list.append(command.enter) # 刪除 cmd_list.append('dy') cmd_list.append(command.enter) cmd = ''.join(cmd_list) # 定義如何根據情況回覆訊息 target_list = [ connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True) ] # 送出 api.connect_core.send( cmd, target_list) if api.is_mailbox_full: api.logout() raise exceptions.MailboxFull() ================================================ FILE: PyPtt/_api_mark_post.py ================================================ from . import _api_util from . import check_value from . import command from . import connect_core from . import data_type from . import exceptions from . import i18n from . import lib_util from . import log from . import screens def mark_post(api, mark_type: int, board: str, post_aid: str, post_index: int, search_type: int, search_condition: str) -> None: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) if not isinstance(mark_type, data_type.MarkType): raise TypeError(f'mark_type must be data_type.MarkType') check_value.check_type(board, str, 'board') if post_aid is not None: check_value.check_type(post_aid, str, 'PostAID') check_value.check_type(post_index, int, 'PostIndex') if not isinstance(search_type, data_type.SearchType): raise TypeError(f'search_type must be data_type.SearchType') if search_condition is not None: check_value.check_type(search_condition, str, 'SearchCondition') if len(board) == 0: raise ValueError(f'board error parameter: {board}') if mark_type != data_type.MarkType.DELETE_D: if post_index != 0 and isinstance(post_aid, str): raise ValueError('wrong parameter index and aid can\'t both input') if post_index == 0 and post_aid is None: raise ValueError('wrong parameter index or aid must input') if search_condition is not None and search_type == 0: raise ValueError('wrong parameter index or aid must input') if search_type == data_type.SearchType.COMMENT: try: S = int(search_condition) except ValueError: raise ValueError(f'wrong parameter search_condition: {search_condition}') check_value.check_range(S, -100, 100, 'search_condition') if post_aid is not None and search_condition is not None: raise ValueError('wrong parameter aid and search_condition can\'t both input') if post_index != 0: newest_index = api.get_newest_index( data_type.NewIndex.BOARD, board=board, search_type=search_type, search_condition=search_condition) check_value.check_index( 'index', post_index, max_value=newest_index) if mark_type == data_type.MarkType.UNCONFIRMED: # 批踢踢兔沒有待證文章功能 QQ if api.config.host == data_type.HOST.PTT2: raise exceptions.HostNotSupport(lib_util.get_current_func_name()) _api_util.check_board( board, check_moderator=True) _api_util.goto_board(api, board) cmd_list = [] if post_aid is not None: cmd_list.append(lib_util.check_aid(post_aid)) cmd_list.append(command.enter) elif post_index != 0: if search_condition is not None: if search_type == data_type.SearchType.KEYWORD: cmd_list.append('/') elif search_type == data_type.SearchType.AUTHOR: cmd_list.append('a') elif search_type == data_type.SearchType.COMMENT: cmd_list.append('Z') elif search_type == data_type.SearchType.MARK: cmd_list.append('G') elif search_type == data_type.SearchType.MONEY: cmd_list.append('A') cmd_list.append(search_condition) cmd_list.append(command.enter) cmd_list.append(str(post_index)) cmd_list.append(command.enter) else: raise ValueError('post_aid and post_index cannot be None at the same time') if mark_type == data_type.MarkType.S: cmd_list.append('L') elif mark_type == data_type.MarkType.D: cmd_list.append('t') elif mark_type == data_type.MarkType.DELETE_D: cmd_list.append(command.ctrl_d) elif mark_type == data_type.MarkType.M: cmd_list.append('m') elif mark_type == data_type.MarkType.UNCONFIRMED: cmd_list.append(command.ctrl_e + 'S') cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('刪除所有標記', log_level=log.INFO, response='y' + command.enter), connect_core.TargetUnit(screens.Target.InBoard, log_level=log.INFO, break_detect=True), ] api.connect_core.send(cmd, target_list) ================================================ FILE: PyPtt/_api_post.py ================================================ from __future__ import annotations from . import _api_util from . import check_value from . import command from . import connect_core from . import exceptions from . import i18n from . import lib_util from . import log def fast_post_step0(api, board: str, title: str, content: str, post_type: int) -> None: _api_util.goto_board(api, board) cmd_list = [] cmd_list.append(command.ctrl_p) cmd_list.append(str(post_type)) cmd_list.append(command.enter) cmd_list.append(str(title)) cmd_list.append(command.enter) cmd_list.append(str(content)) cmd_list.append(command.ctrl_x) cmd_list.append('s') cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('發表文章於【', break_detect=True), connect_core.TargetUnit('使用者不可發言', break_detect=True), connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True), connect_core.TargetUnit('任意鍵繼續', break_detect=True), connect_core.TargetUnit('確定要儲存檔案嗎', break_detect=True) ] index = api.connect_core.fast_send(cmd, target_list) if index < 0: raise exceptions.UnknownError('UnknownError') if index == 1 or index == 2: raise exceptions.NoPermission(i18n.no_permission) def fast_post_step1(api: object, sign_file) -> None: cmd = '\r' target_list = [ connect_core.TargetUnit('發表文章於【', break_detect=True), connect_core.TargetUnit('使用者不可發言', break_detect=True), connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True), connect_core.TargetUnit('任意鍵繼續', break_detect=True), connect_core.TargetUnit('確定要儲存檔案嗎', break_detect=True), connect_core.TargetUnit('x=隨機', response=str(sign_file) + '\r'), ] index = api.connect_core.fast_send(cmd, target_list) if index < 0: raise exceptions.UnknownError('UnknownError') def fast_post( api: object, board: str, title: str, content: str, post_type: int, sign_file) -> None: _api_util.goto_board(api, board) cmd_list = [] cmd_list.append(command.ctrl_p) cmd_list.append(str(post_type)) cmd_list.append(command.enter) cmd_list.append(str(title)) cmd_list.append(command.enter) cmd_list.append(str(content)) cmd_list.append(command.ctrl_x) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('發表文章於【', break_detect=True), connect_core.TargetUnit('使用者不可發言', break_detect=True), connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True), connect_core.TargetUnit('任意鍵繼續', break_detect=True), connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter), connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter), ] index = api.connect_core.fast_send(cmd, target_list) if index < 0: raise exceptions.UnknownError('UnknownError') if index == 1 or index == 2: raise exceptions.NoPermission(i18n.no_permission) sign_file_list = [str(x) for x in range(0, 10)] sign_file_list.append('x') def post(api, board: str, title: str, content: str, title_index: int, sign_file: [str | int]) -> None: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) check_value.check_type(board, str, 'board') check_value.check_type(title_index, int, 'title_index') check_value.check_type(title, str, 'title') check_value.check_type(content, str, 'content') if str(sign_file).lower() not in sign_file_list: raise ValueError(f'wrong parameter sign_file: {sign_file}') _api_util.check_board(api, board) _api_util.goto_board(api, board) log.logger.info(i18n.post) cmd_list = [] cmd_list.append(command.ctrl_p) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('發表文章於【', break_detect=True), connect_core.TargetUnit('使用者不可發言', break_detect=True), connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True), ] index = api.connect_core.send(cmd, target_list) if index < 0: raise exceptions.UnknownError('UnknownError') if index == 1 or index == 2: log.logger.info(i18n.post, '...', i18n.fail) raise exceptions.NoPermission(i18n.no_permission) log.logger.debug(i18n.has_post_permission) content = lib_util.uniform_new_line(content) cmd_list = [] cmd_list.append(str(title_index)) cmd_list.append(command.enter) cmd_list.append(str(title)) cmd_list.append(command.enter) cmd_list.append(command.ctrl_y * 40) cmd_list.append(str(content)) cmd_list.append(command.ctrl_x) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('任意鍵繼續', break_detect=True), connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter), connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter), ] api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_post_timeout) log.logger.info(i18n.post, '...', i18n.success) ================================================ FILE: PyPtt/_api_reply_post.py ================================================ from . import _api_util from . import check_value from . import command from . import connect_core from . import data_type from . import exceptions from . import i18n from . import lib_util from . import log def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: str, sign_file, post_aid: str, post_index: int) -> None: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not isinstance(reply_to, data_type.ReplyTo): raise TypeError(f'ReplyTo must be data_type.ReplyTo') check_value.check_type(board, str, 'board') check_value.check_type(content, str, 'content') if post_aid is not None: check_value.check_type(post_aid, str, 'PostAID') if post_index != 0: newest_index = api.get_newest_index( data_type.NewIndex.BOARD, board=board) check_value.check_index( 'index', post_index, max_value=newest_index) sign_file_list = ['x'] sign_file_list.extend([str(x) for x in range(0, 10)]) if str(sign_file).lower() not in sign_file_list: raise ValueError(f'wrong parameter sign_file: {sign_file}') if post_aid is not None and post_index != 0: raise ValueError('wrong parameter aid and index can\'t both input') _api_util.check_board(api, board) _api_util.goto_board(api, board) cmd_list = [] if post_aid is not None: cmd_list.append(lib_util.check_aid(post_aid)) elif post_index != 0: cmd_list.append(str(post_index)) else: raise ValueError('post_aid and post_index cannot be None at the same time') cmd_list.append(command.enter * 2) cmd_list.append('r') if reply_to == data_type.ReplyTo.BOARD: log.logger.info(i18n.reply_board) reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='F' + command.enter) elif reply_to == data_type.ReplyTo.MAIL: log.logger.info(i18n.reply_mail) reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='M' + command.enter) elif reply_to == data_type.ReplyTo.BOARD_MAIL: log.logger.info(i18n.reply_board_mail) reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='B' + command.enter) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('任意鍵繼續', break_detect=True), connect_core.TargetUnit('◆ 很抱歉, 此文章已結案並標記, 不得回應', log_level=log.INFO, exceptions_=exceptions.CantResponse()), connect_core.TargetUnit('(E)繼續編輯 (W)強制寫入', log_level=log.INFO, response='W' + command.enter), connect_core.TargetUnit('請選擇簽名檔', response=str(sign_file) + command.enter), connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter), connect_core.TargetUnit('編輯文章', log_level=log.INFO, response=str(content) + command.enter + command.ctrl_x), connect_core.TargetUnit('請問要引用原文嗎', log_level=log.DEBUG, response='Y' + command.enter), connect_core.TargetUnit('採用原標題[Y/n]?', log_level=log.DEBUG, response='Y' + command.enter), reply_target_unit, connect_core.TargetUnit('已順利寄出,是否自存底稿', log_level=log.DEBUG, response='Y' + command.enter), ] api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_long_timeout) log.logger.info(i18n.success) ================================================ FILE: PyPtt/_api_search_user.py ================================================ from . import _api_util from . import check_value from . import command from . import connect_core from . import exceptions from . import i18n from . import lib_util from . import log def search_user(api, ptt_id: str, min_page: int, max_page: int) -> list: _api_util.one_thread(api) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) check_value.check_type(ptt_id, str, 'ptt_id') if min_page is not None: check_value.check_index('min_page', min_page) if max_page is not None: check_value.check_index('max_page', max_page) if min_page is not None and max_page is not None: check_value.check_index_range('min_page', min_page, 'max_page', max_page) cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append('T') cmd_list.append(command.enter) cmd_list.append('Q') cmd_list.append(command.enter) cmd_list.append(ptt_id) cmd = ''.join(cmd_list) if min_page is not None: template = min_page else: template = 1 appendstr = ' ' * template cmdtemp = cmd + appendstr target_list = [ connect_core.TargetUnit('任意鍵', break_detect=True)] resultlist = [] log.logger.info(i18n.search_user) while True: api.connect_core.send( cmdtemp, target_list) ori_screen = api.connect_core.get_screen_queue()[-1] # print(OriScreen) # print(len(OriScreen.split('\n'))) if len(ori_screen.split('\n')) == 2: result_id = ori_screen.split('\n')[1] result_id = result_id[result_id.find(' ') + 1:].strip() # print(result_id) resultlist.append(result_id) break else: ori_screen = ori_screen.split('\n')[3:-1] ori_screen = '\n'.join(ori_screen) templist = ori_screen.replace('\n', ' ') while ' ' in templist: templist = templist.replace(' ', ' ') templist = templist.split(' ') resultlist.extend(templist) # print(templist) # print(len(templist)) if len(templist) != 100 and len(templist) != 120: break template += 1 if max_page is not None: if template > max_page: break cmdtemp = ' ' api.connect_core.send( command.enter, [ # 《ID暱稱》 connect_core.TargetUnit('《ID暱稱》', response=command.enter), connect_core.TargetUnit('查詢網友', break_detect=True) ] ) log.logger.info(i18n.success) return list(filter(None, resultlist)) ================================================ FILE: PyPtt/_api_set_board_title.py ================================================ from . import _api_util from . import check_value from . import command from . import connect_core from . import exceptions from . import i18n from . import lib_util def set_board_title(api, board: str, new_title: str) -> None: # 第一支板主專用 api _api_util.one_thread(api) _api_util.goto_board(api, board) if not api._is_login: raise exceptions.RequireLogin(i18n.require_login) if not api.is_registered_user: raise exceptions.UnregisteredUser(lib_util.get_current_func_name()) check_value.check_type(board, str, 'board') check_value.check_type(new_title, str, 'new_title') _api_util.check_board( api, board, check_moderator=True) cmd_list = [] cmd_list.append('I') cmd_list.append(command.ctrl_p) cmd_list.append('b') cmd_list.append(command.enter) cmd_list.append(command.backspace * 31) cmd_list.append(new_title) cmd_list.append(command.enter * 2) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('◆ 已儲存新設定', break_detect=True), connect_core.TargetUnit('◆ 未改變任何設定', break_detect=True), ] api.connect_core.send( cmd, target_list, screen_timeout=api.config.screen_long_timeout) ================================================ FILE: PyPtt/_api_util.py ================================================ from __future__ import annotations import functools import re import threading from typing import Dict, Optional from . import _api_get_board_info from . import command from . import connect_core from . import data_type from . import exceptions from . import log from . import screens def get_content(api, post_mode: bool = True): api.Unconfirmed = False def is_unconfirmed_handler(screen): api.Unconfirmed = True if post_mode: cmd = command.enter * 2 else: cmd = command.enter target_list = [ # 待證實文章 connect_core.TargetUnit('本篇文章內容經站方授權之板務管理人員判斷有尚待證實之處', response=' ', handler=is_unconfirmed_handler), connect_core.TargetUnit(screens.Target.PostEnd, log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit(screens.Target.InPost, log_level=log.DEBUG, break_detect=True), connect_core.TargetUnit(screens.Target.PostNoContent, log_level=log.DEBUG, break_detect=True), # 動畫文章 connect_core.TargetUnit(screens.Target.Animation, response=command.go_main_menu_type_q, break_detect_after_send=True), ] line_from_pattern = re.compile('[\d]+~[\d]+') has_control_code = False control_code_mode = False push_start = False content_start_exist = False content_start_jump = False content_start_jump_set = False first_page = True origin_post = [] stop_dict = dict() while True: index = api.connect_core.send(cmd, target_list) if index == 3 or index == 4: return None, False last_screen = api.connect_core.get_screen_queue()[-1] lines = last_screen.split('\n') last_line = lines[-1] lines.pop() last_screen = '\n'.join(lines) if screens.Target.content_start in last_screen and not content_start_exist: content_start_exist = True if content_start_exist: if not content_start_jump_set: if screens.Target.content_start not in last_screen: content_start_jump = True content_start_jump_set = True else: content_start_jump = False pattern_result = line_from_pattern.search(last_line) if pattern_result is None: control_code_mode = True has_control_code = True else: last_read_line_list = pattern_result.group(0).split('~') last_read_line_a_temp = int(last_read_line_list[0]) last_read_line_b_temp = int(last_read_line_list[1]) if control_code_mode: last_read_line_a = last_read_line_a_temp - 1 last_read_line_b = last_read_line_b_temp - 1 control_code_mode = False if first_page: first_page = False origin_post.append(last_screen) else: # 這裡是根據觀察畫面行數的變化歸納出的神奇公式... # 輸出的結果是要判斷出畫面的最後 x 行是新的文章內容 # # 這裡是 PyPtt 最黑暗最墮落的地方,所有你所知的程式碼守則,在這裡都不適用 # 每除完一次錯誤,我會陷入嚴重的創傷後壓力症候群,而我的腦袋會自動選擇遺忘這裡所有的一切 # 以確保下一個週一,我可以正常上班 # but it works! # print(LastScreen) # print(f'last_read_line_a_temp [{last_read_line_a_temp}]') # print(f'last_read_line_b_temp [{last_read_line_b_temp}]') # print(f'last_read_line_a {last_read_line_a}') # print(f'last_read_line_b {last_read_line_b}') # print(f'GetLineB {last_read_line_a_temp - last_read_line_a}') # print(f'GetLineA {last_read_line_b_temp - last_read_line_b}') # print(f'show line {last_read_line_b_temp - last_read_line_a_temp + 1}') if not control_code_mode: if last_read_line_a_temp in stop_dict: new_content_part = '\n'.join( lines[-stop_dict[last_read_line_a_temp]:]) stop_dict = dict() else: get_line_b = last_read_line_b_temp - last_read_line_b if get_line_b > 0: # print('Type 1') new_content_part = '\n'.join(lines[-get_line_b:]) if index == 1 and len(new_content_part) == get_line_b - 1: new_content_part = '\n'.join(lines[-(get_line_b * 2):]) elif origin_post: last_line_temp = origin_post[-1].strip() try_line = lines[-(get_line_b + 1)].strip() if not last_line_temp.endswith(try_line): new_content_part = try_line + '\n' + new_content_part stop_dict = dict() else: # 駐足現象,LastReadLineB跟上一次相比並沒有改變 if (last_read_line_b_temp + 1) not in stop_dict: stop_dict[last_read_line_b_temp + 1] = 1 stop_dict[last_read_line_b_temp + 1] += 1 get_line_a = last_read_line_a_temp - last_read_line_a if get_line_a > 0: # print(f'Type 2 get_line_a [{get_line_a}]') new_content_part = '\n'.join(lines[-get_line_a:]) else: new_content_part = '\n'.join(lines) else: new_content_part = lines[-1] origin_post.append(new_content_part) log.logger.debug('NewContentPart', new_content_part) if index == 1: if content_start_jump and len(new_content_part) == 0: get_line_b += 1 new_content_part = '\n'.join(lines[-get_line_b:]) origin_post.pop() origin_post.append(new_content_part) break if not control_code_mode: last_read_line_a = last_read_line_a_temp last_read_line_b = last_read_line_b_temp for EC in screens.Target.content_end_list: if EC in last_screen: push_start = True break if push_start: cmd = command.right else: cmd = command.down # print(api.Unconfirmed) origin_post = '\n'.join(origin_post) # OriginPost = [line.strip() for line in OriginPost.split('\n')] # OriginPost = '\n'.join(OriginPost) log.logger.debug('OriginPost', origin_post) return origin_post, has_control_code mail_capacity: Optional[tuple[int, int]] = None def get_mailbox_capacity(api) -> tuple[int, int]: global mail_capacity if mail_capacity is not None: return mail_capacity last_screen = api.connect_core.get_screen_queue()[-1] capacity_line = last_screen.split('\n')[2] log.logger.debug('capacity_line', capacity_line) pattern_result = re.compile('(\d+)/(\d+)').search(capacity_line) if pattern_result is not None: current_capacity = int(pattern_result.group(0).split('/')[0]) max_capacity = int(pattern_result.group(0).split('/')[1]) log.logger.debug('current_capacity', current_capacity) log.logger.debug('max_capacity', max_capacity) mail_capacity = (current_capacity, max_capacity) return current_capacity, max_capacity return 0, 0 # > 1 112/09 ericsk □ [心得] 終於開板了 # ┌── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─┐ # │ 文章代碼(AID): #13cPSYOX (Python) [ptt.cc] [心得] 終於開板了 │ # │ 文章網址: https://www.ptt.cc/bbs/Python/M.1134139170.A.621.html │ # │ 這一篇文章值 2 Ptt幣 │ # └── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─┘ def parse_query_post(api, ori_screen): lock_post = False try: cursor_line = [line for line in ori_screen.split( '\n') if line.strip().startswith(api.cursor)][0] except Exception as e: print(api.cursor) print(ori_screen) raise e post_author = cursor_line if '□' in post_author: post_author = post_author[:post_author.find('□')].strip() elif 'R:' in post_author: post_author = post_author[:post_author.find('R:')].strip() elif ' 轉 ' in post_author: post_author = post_author[:post_author.find('轉')].strip() elif ' 鎖 ' in post_author: post_author = post_author[:post_author.find('鎖')].strip() lock_post = True post_author = post_author[post_author.rfind(' '):].strip() post_title = cursor_line if ' □ ' in post_title: post_title = post_title[post_title.find('□') + 1:].strip() elif ' R:' in post_title: post_title = post_title[post_title.find('R:'):].strip() elif ' 轉 ' in post_title: post_title = post_title[post_title.find('轉') + 1:].strip() post_title = f'Fw: {post_title}' elif ' 鎖 ' in post_title: post_title = post_title[post_title.find('鎖') + 1:].strip() ori_screen_temp = ori_screen[ori_screen.find('┌──'):] ori_screen_temp = ori_screen_temp[:ori_screen_temp.find('└──')] aid_line = [line for line in ori_screen.split( '\n') if line.startswith('│ 文章代碼(AID)')] post_aid = None if len(aid_line) == 1: aid_line = aid_line[0] pattern = re.compile('#[\w|-]+') pattern_result = pattern.search(aid_line) post_aid = pattern_result.group(0)[1:] pattern = re.compile('文章網址: https:[\S]+html') pattern_result = pattern.search(ori_screen_temp) if pattern_result is None: post_web = None else: post_web = pattern_result.group(0)[6:] pattern = re.compile('這一篇文章值 [\d]+ Ptt幣') pattern_result = pattern.search(ori_screen_temp) if pattern_result is None: # 特殊文章無價格 post_money = -1 else: post_money = pattern_result.group(0)[7:] post_money = post_money[:post_money.find(' ')] post_money = int(post_money) pattern = re.compile('[\d]+\/[\d]+') pattern_result = pattern.search(cursor_line) if pattern_result is None: list_date = None else: list_date = pattern_result.group(0) list_date = list_date[-5:] # print(list_date) # > 7485 9 8/09 CodingMan □ [閒聊] PTT Library 更新 # > 79189 M 1 9/17 LittleCalf □ [公告] 禁言退文公告 # >781508 +爆 9/17 jodojeda □ [新聞] 國人吃魚少 學者:應把吃魚當成輕鬆愉快 # >781406 +X1 9/17 kingofage111 R: [申請] ReDmango 請辭Gossiping板主職務 pattern = re.compile('[\d]+') pattern_result = pattern.search(cursor_line) post_index = 0 if pattern_result is not None: post_index = int(pattern_result.group(0)) push_number = cursor_line push_number = push_number[7:11] push_number = push_number.split(' ') push_number = list(filter(None, push_number)) if len(push_number) == 0: push_number = None else: push_number = push_number[-1] if push_number.startswith('爆') or push_number.startswith('~爆'): push_number = '爆' if push_number.startswith('+') or push_number.startswith('~'): push_number = push_number[1:] if push_number.lower().startswith('m'): push_number = push_number[1:] if push_number.lower().startswith('!'): push_number = push_number[1:] if push_number.lower().startswith('s'): push_number = push_number[1:] if push_number.lower().startswith('='): push_number = push_number[1:] if len(push_number) == 0: push_number = None log.logger.debug('PostAuthor', post_author) log.logger.debug('PostTitle', post_title) log.logger.debug('PostAID', post_aid) log.logger.debug('PostWeb', post_web) log.logger.debug('PostMoney', post_money) log.logger.debug('ListDate', list_date) log.logger.debug('PushNumber', push_number) return lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index def get_search_condition_cmd(index_type: data_type.NewIndex, search_list: Optional[list] = None): cmd_list = [] if not search_list: return cmd_list for search_type, search_condition in search_list: if search_type == data_type.SearchType.KEYWORD: cmd_list.append('/') elif search_type == data_type.SearchType.AUTHOR: cmd_list.append('a') elif search_type == data_type.SearchType.MARK: cmd_list.append('G') elif index_type == data_type.NewIndex.BOARD: if search_type == data_type.SearchType.COMMENT: cmd_list.append('Z') elif search_type == data_type.SearchType.MONEY: cmd_list.append('A') else: continue else: continue cmd_list.append(search_condition) cmd_list.append(command.enter) return cmd_list def goto_board(api, board: str, refresh: bool = False, end: bool = False) -> None: cmd_list = [] cmd_list.append(command.go_main_menu) cmd_list.append('qs') cmd_list.append(board) cmd_list.append(command.enter) cmd_list.append(command.space) cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit('任意鍵', log_level=log.DEBUG, response=' '), connect_core.TargetUnit('互動式動畫播放中', log_level=log.DEBUG, response=command.ctrl_c), connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True), ] if refresh: current_refresh = True else: if board.lower() in api._goto_board_list: current_refresh = True else: current_refresh = False api._goto_board_list.append(board.lower()) api.connect_core.send(cmd, target_list, refresh=current_refresh) if end: cmd_list = [] cmd_list.append('1') cmd_list.append(command.enter) cmd_list.append('$') cmd = ''.join(cmd_list) target_list = [ connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True), ] api.connect_core.send(cmd, target_list) def one_thread(api): current_thread_id = threading.get_ident() if current_thread_id != api._thread_id: raise exceptions.MultiThreadOperated() @functools.lru_cache(maxsize=64) def check_board(api, board: str, check_moderator: bool = False) -> Dict: if board.lower() not in api._exist_board_list: board_info = _api_get_board_info.get_board_info(api, board, get_post_kind=False, call_by_others=False) api._exist_board_list.append(board.lower()) api._board_info_list[board.lower()] = board_info moderators = board_info[data_type.BoardField.moderators] moderators = [x.lower() for x in moderators] api._moderators[board.lower()] = moderators api._board_info_list[board.lower()] = board_info if check_moderator: if api.ptt_id.lower() not in api._moderators[board.lower()]: raise exceptions.NeedModeratorPermission(board) return api._board_info_list[board.lower()] ================================================ FILE: PyPtt/check_value.py ================================================ from . import i18n from . import log def check_type(value, value_type, name) -> None: if not isinstance(value, value_type): if value_type is str: raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_string}, but got {value}') elif value_type is int: raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_integer}, but got {value}') elif value_type is bool: raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_boolean}, but got {value}') else: raise TypeError(f'[PyPtt] {name} {i18n.must_be} {value_type}, but got {value}') def check_range(value, min_value, max_value, name) -> None: check_type(value, int, name) check_type(min_value, int, 'min_value') check_type(max_value, int, 'max_value') if min_value <= value <= max_value: return raise ValueError(f'{name} {value} {i18n.must_between} {min_value} ~ {max_value}') def check_index(name, index, max_value=None) -> None: check_type(index, int, name) if index < 1: raise ValueError(f'{name} {i18n.must_bigger_than} 0') if max_value is not None: if index > max_value: log.logger.info('index', index) log.logger.info('max_value', max_value) raise ValueError(f'{name} {index} {i18n.must_between} 0 ~ {max_value}') def check_index_range(start_name, start_index, end_name, end_index, max_value=None) -> None: check_type(start_index, int, start_name) check_type(end_index, int, end_name) if start_index < 1: raise ValueError(f'{start_name} {start_index} {i18n.must_bigger_than} 0') if end_index <= 1: raise ValueError(f'{end_name} {end_index} {i18n.must_bigger_than} 1') if start_index > end_index: raise ValueError(f'{end_name} {end_index} {i18n.must_bigger_than} {start_name} {start_index}') if max_value is not None: if start_index > max_value: raise ValueError(f'{start_name} {start_index} {i18n.must_small_than} {max_value}') if end_index > max_value: raise ValueError(f'{end_name} {end_index} {i18n.must_small_than} {max_value}') if __name__ == '__main__': QQ = str if QQ is str: print('1') if QQ == str: print('2') if isinstance('', QQ): print('3') ================================================ FILE: PyPtt/command.py ================================================ # http://www.physics.udel.edu/~watson/scen103/ascii.html enter = '\r' tab = '\t' ctrl_c = '\x03' ctrl_d = '\x04' ctrl_e = '\x05' ctrl_f = '\x06' ctrl_h = '\x08' ctrl_l = '\x0C' ctrl_p = '\x10' ctrl_u = '\x15' ctrl_x = '\x18' ctrl_y = '\x19' ctrl_z = '\x1A' star = '\x2A' up = '\x1b\x4fA' down = '\x1b\x4fB' right = '\x1b\x4fC' left = '\x1b\x4fD' space = ' ' query_post = 'Q' comment = 'X' go_main_menu = ' ' + left * 5 go_main_menu_type_q = 'q' * 5 refresh = ctrl_l control_code = ctrl_u + star backspace = ctrl_h ================================================ FILE: PyPtt/config.py ================================================ from . import data_type from . import log class Config: # retry_wait_time 秒後重新連線 retry_wait_time = 3 # ScreenLTimeOut 秒後判定此畫面沒有可辨識的目標 screen_timeout = 3.0 # screen_long_timeout 秒後判定此畫面沒有可辨識的目標 # 適用於需要特別等待的情況,例如: 剔除其他登入等等 # 建議不要低於 10 秒,剔除其他登入最長可能會花費約六到七秒 screen_long_timeout = 10.0 # screen_post_timeout 秒後判定此畫面沒有可辨識的目標 # 適用於貼文等待的情況,建議不要低於 60 秒 screen_post_timeout = 60.0 # 預設語言 language = data_type.Language.MANDARIN # 預設 log 等級 log_level = log.INFO # 預設不剔除其他登入 kick_other_session = False # 預設登入 PTT1 host = data_type.HOST.PTT1 # 預設採用 websockets connect_mode = None # 預設使用 23 port = 23 logger_callback = None LOGGER_CONFIG = { } ================================================ FILE: PyPtt/connect_core.py ================================================ from __future__ import annotations import asyncio import ssl import telnetlib import threading import time import traceback import warnings from typing import Any import websockets import websockets.exceptions import websockets.http import PyPtt from . import command from . import data_type from . import exceptions from . import i18n from . import log from . import screens websockets.http.USER_AGENT += f' PyPtt/{PyPtt.__version__}' ssl_context = ssl.create_default_context() class TargetUnit: def __init__(self, detect_target, log_level: log.LogLevel = None, response: [Any | str] = '', break_detect=False, break_detect_after_send=False, exceptions_=None, refresh=True, secret=False, handler=None, max_match: int = 0): self.detect_target = detect_target if log_level is None: self.log_level = log.INFO else: self.log_level = log_level self._response_func = response self._break_detect = break_detect self._exception = exceptions_ self._refresh = refresh self._break_after_send = break_detect_after_send self._secret = secret self._Handler = handler self._max_match = max_match self._current_match = 0 def is_match(self, screen: str) -> bool: if self._current_match >= self._max_match > 0: return False if isinstance(self.detect_target, str): if self.detect_target in screen: self._current_match += 1 return True return False elif isinstance(self.detect_target, list): for Target in self.detect_target: if Target not in screen: return False self._current_match += 1 return True def get_detect_target(self): return self.detect_target def get_log_level(self): return self.log_level def get_response(self, screen: str) -> str: if callable(self._response_func): return self._response_func(screen) return self._response_func def is_break(self) -> bool: return self._break_detect def raise_exception(self): if isinstance(self._exception, Exception): raise self._exception def is_refresh(self) -> bool: return self._refresh def is_break_after_send(self) -> bool: return self._break_after_send def is_secret(self) -> bool: return self._secret class RecvData: def __init__(self): self.data = None async def websocket_recv_func(core, recv_data_obj): recv_data_obj.data = await core.recv() async def websocket_receiver(core, screen_timeout, recv_data_obj): # Wait for at most 1 second await asyncio.wait_for( websocket_recv_func(core, recv_data_obj), timeout=screen_timeout) class ReceiveDataQueue(object): def __init__(self): self._ReceiveDataQueue = [] def add(self, screen): self._ReceiveDataQueue.append(screen) self._ReceiveDataQueue = self._ReceiveDataQueue[-10:] def get(self, last=1): return self._ReceiveDataQueue[-last:] class API(object): def __init__(self, config): self.current_encoding = 'big5uao' self.config = config self._RDQ = ReceiveDataQueue() self._UseTooManyResources = TargetUnit(screens.Target.use_too_many_resources, exceptions_=exceptions.UseTooManyResources()) def connect(self) -> None: def _wait(): for i in range(self.config.retry_wait_time): if self.config.host == data_type.HOST.PTT1: log.logger.info(i18n.prepare_connect_again, i18n.PTT, str(self.config.retry_wait_time - i)) elif self.config.host == data_type.HOST.PTT2: log.logger.info(i18n.prepare_connect_again, i18n.PTT2, str(self.config.retry_wait_time - i)) elif self.config.host == data_type.HOST.LOCALHOST: log.logger.info(i18n.prepare_connect_again, i18n.localhost, str(self.config.retry_wait_time - i)) else: log.logger.info(i18n.prepare_connect_again, self.config.host, str(self.config.retry_wait_time - i)) time.sleep(1) warnings.filterwarnings("ignore", category=DeprecationWarning) self.current_encoding = 'big5uao' # self.log.py.info(i18n.connect_core, i18n.active) if self.config.host == data_type.HOST.PTT1: telnet_host = 'ptt.cc' websocket_host = 'wss://ws.ptt.cc/bbs/' websocket_origin = 'https://term.ptt.cc' elif self.config.host == data_type.HOST.PTT2: telnet_host = 'ptt2.cc' websocket_host = 'wss://ws.ptt2.cc/bbs/' websocket_origin = 'https://term.ptt2.cc' elif self.config.host == data_type.HOST.LOCALHOST: telnet_host = 'localhost' websocket_host = 'wss://localhost' websocket_origin = 'https://term.ptt.cc' else: telnet_host = self.config.host websocket_host = f'wss://{self.config.host}' websocket_origin = 'https://term.ptt.cc' connect_success = False for _ in range(2): try: if self.config.connect_mode == data_type.ConnectMode.TELNET: self._core = telnetlib.Telnet(telnet_host, self.config.port) else: if not threading.current_thread() is threading.main_thread(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) log.logger.debug('USER_AGENT', websockets.http.USER_AGENT) self._core = asyncio.get_event_loop().run_until_complete( websockets.connect( websocket_host, origin=websocket_origin, ssl=ssl_context)) connect_success = True except Exception as e: traceback.print_tb(e.__traceback__) print(e) if self.config.host == data_type.HOST.PTT1: log.logger.info(i18n.connect, i18n.PTT, i18n.fail) elif self.config.host == data_type.HOST.PTT2: log.logger.info(i18n.connect, i18n.PTT2, i18n.fail) elif self.config.host == data_type.HOST.LOCALHOST: log.logger.info(i18n.connect, i18n.localhost, i18n.fail) else: log.logger.info(i18n.connect, self.config.host, i18n.fail) _wait() continue break if not connect_success: raise exceptions.ConnectError(self.config) def _decode_screen(self, receive_data_buffer, start_time, target_list, is_secret, refresh, msg): break_detect_after_send = False use_too_many_res = False vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding) screen = vt100_p.screen find_target = False target_index = -1 for target in target_list: condition = target.is_match(screen) if condition: if target._Handler is not None: target._Handler(screen) if len(screen) > 0: screens.show(self.config, screen) self._RDQ.add(screen) if target == self._UseTooManyResources: use_too_many_res = True # print(f'1 {use_too_many_res}') break target.raise_exception() find_target = True end_time = time.time() log.logger.debug(i18n.spend_time, round(end_time - start_time, 3)) if target.is_break(): target_index = target_list.index(target) break msg = target.get_response(screen) add_refresh = False if target.is_refresh(): add_refresh = True elif refresh: add_refresh = True if add_refresh: if not msg.endswith(command.refresh): msg = msg + command.refresh is_secret = target.is_secret() if target.is_break_after_send(): # break_index = target_list.index(target) break_detect_after_send = True break return screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index def send(self, msg: str, target_list: list, screen_timeout: int = 0, refresh: bool = True, secret: bool = False) -> int: if not all(isinstance(T, TargetUnit) for T in target_list): raise ValueError('Item of TargetList must be TargetUnit') if self._UseTooManyResources not in target_list: target_list.append(self._UseTooManyResources) if screen_timeout == 0: current_screen_timeout = self.config.screen_timeout else: current_screen_timeout = screen_timeout break_detect_after_send = False is_secret = secret use_too_many_res = False while True: if refresh and not msg.endswith(command.refresh): msg = msg + command.refresh try: msg = msg.encode('utf-8', 'replace') except AttributeError: pass except Exception as e: traceback.print_tb(e.__traceback__) print(e) msg = msg.encode('utf-8', 'replace') if is_secret: log.logger.debug(i18n.send_msg, i18n.hide_sensitive_info) else: log.logger.debug(i18n.send_msg, str(msg)) if self.config.connect_mode == data_type.ConnectMode.TELNET: try: self._core.read_very_eager() self._core.write(msg) except EOFError: raise exceptions.ConnectionClosed() else: try: asyncio.get_event_loop().run_until_complete( self._core.send(msg)) except websockets.exceptions.ConnectionClosedError: raise exceptions.ConnectionClosed() except RuntimeError: raise exceptions.ConnectionClosed() except websockets.exceptions.ConnectionClosedOK: raise exceptions.ConnectionClosed() if break_detect_after_send: return -1 msg = '' receive_data_buffer = bytes() start_time = time.time() mid_time = time.time() while mid_time - start_time < current_screen_timeout: # print(1) recv_data_obj = RecvData() if self.config.connect_mode == data_type.ConnectMode.TELNET: try: recv_data_obj.data = self._core.read_very_eager() except EOFError: return -1 else: try: asyncio.get_event_loop().run_until_complete( websocket_receiver( self._core, current_screen_timeout, recv_data_obj)) except websockets.exceptions.ConnectionClosed: if use_too_many_res: raise exceptions.UseTooManyResources() raise exceptions.ConnectionClosed() except websockets.exceptions.ConnectionClosedOK: raise exceptions.ConnectionClosed() except asyncio.TimeoutError: return -1 except RuntimeError: raise exceptions.ConnectionClosed() receive_data_buffer += recv_data_obj.data screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \ self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg) if self.current_encoding == 'big5uao' and not find_target: self.current_encoding = 'utf-8' screen_, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \ self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg) if find_target: screen = screen_ else: self.current_encoding = 'big5uao' # print(4) if target_index != -1: return target_index if use_too_many_res: continue if find_target: break if len(screen) > 0: screens.show(self.config, screen) self._RDQ.add(screen) # print(6) mid_time = time.time() if not find_target: return -1 return -2 def close(self): if self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS: asyncio.get_event_loop().run_until_complete(self._core.close()) else: self._core.close() def get_screen_queue(self) -> list: return self._RDQ.get(1) ================================================ FILE: PyPtt/data_type.py ================================================ import time from enum import auto from AutoStrEnum import AutoStrEnum class Language: MANDARIN = 'zh_TW' ENGLISH = 'en_US' class ConnectMode(AutoStrEnum): TELNET = auto() WEBSOCKETS = auto() class SearchType(AutoStrEnum): """文章搜尋類型""" NOPE = auto() # 搜尋關鍵字 / ? KEYWORD = auto() # 搜尋作者 a AUTHOR = auto() # 搜尋推文數 Z COMMENT = auto() # 搜尋標記 G MARK = auto() # 搜尋稿酬 A MONEY = auto() class ReplyTo(AutoStrEnum): # 回文類型 BOARD = auto() MAIL = auto() BOARD_MAIL = auto() class CommentType(AutoStrEnum): PUSH = auto() BOO = auto() ARROW = auto() class UserField(AutoStrEnum): ptt_id = auto() money = auto() login_count = auto() account_verified = auto() legal_post = auto() illegal_post = auto() activity = auto() mail = auto() last_login_date = auto() last_login_ip = auto() five_chess = auto() chess = auto() signature_file = auto() class CommentField(AutoStrEnum): type = auto() author = auto() content = auto() ip = auto() time = auto() class PostStatus(AutoStrEnum): EXISTS = auto() DELETED_BY_AUTHOR = auto() DELETED_BY_MODERATOR = auto() DELETED_BY_UNKNOWN = auto() class PostField(AutoStrEnum): board = auto() aid = auto() index = auto() author = auto() date = auto() title = auto() content = auto() money = auto() url = auto() ip = auto() comments = auto() post_status = auto() list_date = auto() has_control_code = auto() pass_format_check = auto() location = auto() push_number = auto() is_lock = auto() full_content = auto() is_unconfirmed = auto() # class WaterballInfo: # def __init__(self, waterball_type, target, content, date): # self.type: int = parse_para(int, waterball_type) # self.target: str = parse_para(str, target) # self.content: str = parse_para(str, content) # self.date: str = parse_para(str, date) class Cursor: # 舊式游標 OLD: str = '●' # 新式游標 NEW: str = '>' class NewIndex(AutoStrEnum): # 看板 BOARD = auto() # 信箱 MAIL = auto() # 網頁,尚不支援 # WEB = auto() class HOST(AutoStrEnum): # 批踢踢萬 PTT1 = auto() # 批踢踢兔 PTT2 = auto() # 本機測試用 LOCALHOST = auto() class MarkType(AutoStrEnum): # s 文章 S = auto() # 標記文章 D = auto() # 刪除標記文章 DELETE_D = auto() # M 起來 M = auto() # 待證實文章 UNCONFIRMED = auto() class FavouriteBoardField(AutoStrEnum): board = auto() type = auto() title = auto() class MailField(AutoStrEnum): origin_mail = auto() author = auto() title = auto() date = auto() content = auto() ip = auto() location = auto() is_red_envelope = auto() class BoardField(AutoStrEnum): board = auto() online_user = auto() mandarin_des = auto() moderators = auto() open_status = auto() into_top_ten_when_hide = auto() can_non_board_members_post = auto() can_reply_post = auto() self_del_post = auto() can_comment_post = auto() can_boo_post = auto() can_fast_push = auto() min_interval_between_comments = auto() is_comment_record_ip = auto() is_comment_aligned = auto() can_moderators_del_illegal_content = auto() does_tran_post_auto_recorded_and_require_post_permissions = auto() is_cool_mode = auto() is_require18 = auto() require_login_time = auto() require_illegal_post = auto() # post_kind = auto() post_kind_list = auto() class Compare(AutoStrEnum): BIGGER = auto() SAME = auto() SMALLER = auto() UNKNOWN = auto() class TimedDict: def __init__(self, timeout: int = 0): self.timeout = timeout self.data = {} self.timestamps = {} def __setitem__(self, key, value): self.data[key] = value self.timestamps[key] = time.time() def __getitem__(self, key): if key not in self.data: raise KeyError(key) timestamp = self.timestamps[key] if time.time() - timestamp > self.timeout > 0: del self.data[key] del self.timestamps[key] raise KeyError(key) return self.data[key] def __contains__(self, key): try: self[key] except KeyError: return False else: return True def __len__(self): self.cleanup() return len(self.data) def cleanup(self): if self.timeout == 0: return now = time.time() to_remove = [key for key, timestamp in self.timestamps.items() if now - timestamp > self.timeout > 0] for key in to_remove: del self.data[key] del self.timestamps[key] ================================================ FILE: PyPtt/exceptions.py ================================================ from . import data_type from . import i18n class Error(Exception): pass class UnknownError(Error): def __init__(self, message): self.message = message def __str__(self): return self.message class RequireLogin(Error): def __init__(self, message): self.message = message def __str__(self): return self.message class NoPermission(Error): def __init__(self, message): self.message = message def __str__(self): return self.message class LoginError(Error): def __init__(self): self.message = i18n.login_fail def __str__(self): return self.message class NoFastComment(Error): def __init__(self): self.message = i18n.no_fast_comment def __str__(self): return self.message class NoSuchUser(Error): def __init__(self, user): self.message = i18n.no_such_user + ': ' + user def __str__(self): return self.message class NoSuchMail(Error): def __init__(self): self.message = i18n.no_such_mail def __str__(self): return self.message # class UserOffline(Error): # def __init__(self, user): # self.message = i18n.user_offline + ': ' + user # # def __str__(self): # return self.message # class ParseError(Error): # def __init__(self, screen): # self.message = screen # # def __str__(self): # return self.message class NoMoney(Error): def __init__(self): self.message = i18n.no_money def __str__(self): return self.message class NoSuchBoard(Error): def __init__(self, config, board): if config.host == data_type.HOST.PTT1: self.message = [ i18n.PTT, i18n.no_such_board ] else: self.message = [ i18n.PTT2, i18n.no_such_board ] if config.language == data_type.Language.MANDARIN: self.message = ''.join(self.message) + ': ' + board else: self.message = ' '.join(self.message) + ': ' + board def __str__(self): return self.message class ConnectionClosed(Error): def __init__(self): self.message = i18n.connection_closed def __str__(self): return self.message class UnregisteredUser(Error): def __init__(self, api_name): self.message = i18n.unregistered_user_cant_use_this_api + ': ' + api_name def __str__(self): return self.message class MultiThreadOperated(Error): def __init__(self): self.message = i18n.multi_thread_operate def __str__(self): return self.message class WrongIDorPassword(Error): def __init__(self): self.message = i18n.wrong_id_pw def __str__(self): return self.message class WrongPassword(Error): def __init__(self): self.message = i18n.error_pw def __str__(self): return self.message class LoginTooOften(Error): def __init__(self): self.message = i18n.login_too_often def __str__(self): return self.message class UseTooManyResources(Error): def __init__(self): self.message = i18n.use_too_many_resources def __str__(self): return self.message class HostNotSupport(Error): def __init__(self, api): self.message = f'{i18n.ptt2_not_support}: {api}' def __str__(self): return self.message class CantComment(Error): def __init__(self): self.message = i18n.no_comment def __str__(self): return self.message class CantResponse(Error): def __init__(self): self.message = i18n.no_response def __str__(self): return self.message class NeedModeratorPermission(Error): def __init__(self, board): self.message = f'{i18n.need_moderator_permission}: {board}' def __str__(self): return self.message class ConnectError(Error): def __init__(self, config): self.message = i18n.connect_fail def __str__(self): return self.message class NoSuchPost(Error): def __init__(self, board, aid): self.message = i18n.replace( i18n.no_such_post, board, aid) def __str__(self): return self.message class CanNotUseSearchPostCode(Error): """ 此狀態下無法使用搜尋文章代碼(AID)功能 """ def __init__(self): self.message = i18n.can_not_use_search_post_code_f def __str__(self): return self.message class UserHasPreviouslyBeenBanned(Error): def __init__(self): self.message = i18n.user_has_previously_been_banned def __str__(self): return self.message class MailboxFull(Error): def __init__(self): self.message = i18n.mail_box_full def __str__(self): return self.message class NoSearchResult(Error): def __init__(self): self.message = i18n.no_search_result def __str__(self): return self.message # 此帳號已設定為只能使用安全連線 class OnlySecureConnection(Error): def __init__(self): self.message = i18n.only_secure_connection def __str__(self): return self.message class SetContactMailFirst(Error): def __init__(self): self.message = i18n.set_contact_mail_first def __str__(self): return self.message class ResetYourContactEmail(Error): def __init__(self): self.message = i18n.reset_your_contact_email def __str__(self): return self.message ================================================ FILE: PyPtt/i18n.py ================================================ import os import random import yaml from . import __version__ from . import data_type locale_pool = { data_type.Language.ENGLISH, data_type.Language.MANDARIN } _script_path = os.path.dirname(os.path.abspath(__file__)) _lang_data = {} mapping = { '{version}': __version__, } def replace(string, *args): for i in range(len(args)): target = f'{args[i]}' string = string.replace(f'_target{i}_', target) return string def init(locale: str, cache: bool = False) -> None: if locale not in locale_pool: raise ValueError(f'Unknown locale: {locale}') language_file = f'{_script_path}/lang/{locale}.yaml' if not os.path.exists(language_file): raise ValueError(f'Unknown locale file: {language_file}') with open(language_file, "r") as f: string_data = yaml.safe_load(f) for k, v in string_data.items(): if isinstance(v, list): v = random.choice(v) elif isinstance(v, str): pass else: raise ValueError(f'Unknown string data type: {v}') if locale == data_type.Language.ENGLISH: v = v[0].upper() + v[1:] for mk, mv in mapping.items(): v = v.replace(mk, mv) globals()[k] = v if cache: global _lang_data _lang_data[k] = v ================================================ FILE: PyPtt/lang/en_US.yaml ================================================ PTT: PTT PTT2: PTT2 active: Active author: Author board: Board can_not_use_search_post_code_f: This status can not use the search PostField code function catch_bottom_post_success: Catch bottom post success change_pw: Change password comment: Comment comment_content: Comment content comment_date: Comment date comment_id: Comment id connect: Connect connect_core: Connect core connect_fail: Connect fail connect_mode_TELNET: Telnet connect_mode_WEBSOCKET: WebSocket connection_closed: Connection Closed content: Content current_version: Current version date: Date delete_post: Delete post development_version: Running development version done: Done english_module: English error_pw: Wrong password fail: Fail find_newest_index: Find newest index get_board_info: Get board info _target0_ get_favourite_board_list: Query favourite board list get_mail: Get mail give_money_to: give _target0_ _target1_ P coins goodbye: - good bye - bye - see you - catch you later - I hate to run, but... - Until we meet again, I will wait has_comment_permission: User has permission to comment has_new_mail_goto_main_menu: New mail! Back to main menu has_post_permission: Have permission to post hide_sensitive_info: Hide sensitive info in_login_process_please_wait: In login process, please wait initialization: Init kick_other_login: Kick other login latest_version: Running the latest version localhost: Localhost login_fail: Login fail login_id: Login id login_success: Login ... success login_too_often: Login too often logout: Logout mail_box_full: Mail box is full mandarin_module: Mandarin multi_thread_operate: Do not use a multi-thread to operate a PyPtt object must_be: Must be must_be_a_boolean: Must be a boolean must_be_a_integer: Must be a integer must_be_a_string: Must be a string must_between: Must between must_bigger_than: Must bigger than must_small_than: Must smaller than need_moderator_permission: Need moderator permission new_cursor: New cursor new_version: There is a new version no_comment: No comment no_fast_comment: No fast comment no_money: Not enough PTT coins no_permission: User Has No Permission no_response: This post has been closed and marked, no response no_search_result: No Search Result no_such_board: No such board no_such_mail: No such mail no_such_post: In _target0_, the post code is not EXISTS _target1_ no_such_user: No such user not_kick_other_login: Not kick other login not_push_aligned: No push aligned not_record_ip: Not record IP old_cursor: Old cursor only_secure_connection: Skip registration form picks_in_register: Registration application processing order post: Post article post_deleted: Post has been deleted prepare_connect_again: Prepare connect again ptt2_not_support: PTT2 not support this api push_aligned: Push aligned record_ip: Record ip reply_board: Respond to the BoardField reply_board_mail: Respond to the board and the mailbox of author reply_mail: Respond to the mailbox of author require_login: Please login first reset_your_contact_email: Please reset your contact email retry: Retry search_user: Search user send_mail: Send mail send_msg: Send msg set_connect_host: Set up the connect host set_connect_mode: Set up the connect mode set_contact_mail_first: Password can only be changed after setting the contact mailbox set_up_lang_module: Set up language module spend_time: Spend time substandard_post: Substandard post success: Success title: Title transaction_cancelled: The transaction is cancelled! unregistered_user_cant_use_all_api: Unregistered UserField Can't Use All API unregistered_user_cant_use_this_api: Unregistered UserField Can't Use This API update_remote_version: Fetching latest version use_mailbox_api_will_logout_after_execution: If you use mailbox related functions, you will be logged out automatically after execution use_too_many_resources: Use too many resources user_has_previously_been_banned: User has previously been banned user_offline: User offline wait_for_no_fast_comment: Because no fast comment, wait 5 sec welcome: PyPtt v _target0_ developed by CodingMan wrong_id_pw: Wrong id or pw ================================================ FILE: PyPtt/lang/zh_TW.yaml ================================================ PTT: 批踢踢 PTT2: 批踢踢兔 active: 啟動 author: 作者 board: 看板 can_not_use_search_post_code_f: 此狀態下無法使用搜尋文章代碼(AID)功能 catch_bottom_post: 取得置底文章 change_pw: 變更密碼 comment: 推文 comment_content: 推文內文 comment_date: 推文日期 comment_id: 推文帳號 connect: 連線 connect_core: 連線核心 connect_fail: 連線失敗 connect_mode_TELNET: Telnet connect_mode_WEBSOCKET: WebSocket connection_closed: 連線已經被關閉 content: 內文 current_version: 目前版本 date: 日期 delete_post: 刪除文章 development_version: 正在執行開發版本 done: 完成 english_module: 英文 error_pw: 密碼不正確 fail: 失敗 find_newest_index: 找到最新編號 get_board_info: 取得看板資訊 _target0_ get_favourite_board_list: 取得我的最愛 get_mail: 取得信件 give_money_to: 給 _target0_ _target1_ P 幣 goodbye: - 再見 - 下次再見 - 再會 - 祝平安 - 謝謝你,我很開心 - 我們會再見面的 has_comment_permission: 確認擁有推文權限 has_new_mail_goto_main_menu: 有新信,回到主選單 has_post_permission: 確認擁有貼文權限 hide_sensitive_info: 隱藏敏感資訊 in_login_process_please_wait: 登入中,請稍候 initialization: 初始化 kick_other_login: 強制執行剔除其他登入 latest_version: 正在執行最新版本 localhost: 本機 login_fail: 登入失敗 login_id: 登入帳號 login_success: 登入 ... 成功 login_too_often: 登入太頻繁 logout: 登出 mail_box_full: 郵件已滿 mandarin_module: 繁體中文 multi_thread_operate: 請勿使用多線程同時操作一個 PyPtt 物件 must_be: 必須為 must_be_a_boolean: 必須為布林值 must_be_a_integer: 必須為數字 must_be_a_string: 必須為字串 must_between: 必須介於 must_bigger_than: 必須大於 must_small_than: 必須小於 need_moderator_permission: 需要板主權限 new_cursor: 新式游標 new_version: 有新版本 no_comment: 禁止推薦 no_fast_comment: 禁止快速連續推文 no_money: PTT 幣不足 no_permission: 使用者沒有權限 no_response: 很抱歉, 此文章已結案並標記, 不得回應 no_search_result: 沒有搜尋結果 no_such_board: 無該看板 no_such_mail: 無此信件 no_such_post: _target0_ 板找不到這個文章代碼 _target1_ no_such_user: 無該使用者 not_kick_other_login: 不剔除其他登入 not_push_aligned: 無推文對齊 not_record_ip: 不紀錄 IP old_cursor: 舊式游標 only_secure_connection: 跳過填寫註冊單 picks_in_register: 註冊申請單處理順位 post: 發佈文章 post_deleted: 文章已經被刪除 prepare_connect_again: 準備再次連線 ptt2_not_support: 批踢踢兔不支援此功能 push_aligned: 推文對齊 record_ip: 紀錄 IP reply_board: 回應至看板 reply_board_mail: 回應至看板與作者信箱 reply_mail: 回應至作者信箱 require_login: 請先登入 reset_your_contact_email: 請重新設定您的聯絡信箱 retry: 重試 search_user: 搜尋使用者 send_mail: 寄信 send_msg: 送出訊息 set_connect_host: 設定連線主機 set_connect_mode: 設定連線模式 set_contact_mail_first: 請先設定聯絡信箱後才能修改密碼 set_up_lang_module: 設定語言模組 spend_time: 花費時間 substandard_post: 不合規範文章 success: 成功 title: 標題 transaction_cancelled: 交易取消! unregistered_user_cant_use_all_api: 未註冊使用者,將無法使用全部功能 unregistered_user_cant_use_this_api: 未註冊使用者,無法使用此功能 update_remote_version: 確認最新版本 use_mailbox_api_will_logout_after_execution: 如果使用信箱相關功能,將執行後自動登出 use_too_many_resources: 耗用過多資源 user_has_previously_been_banned: 使用者之前已被禁言 user_offline: 使用者離線 wait_for_no_fast_comment: 因禁止快速連續推文,所以等待五秒 welcome: PyPtt v _target0_ 由 CodingMan 開發 wrong_id_pw: 帳號密碼錯誤 ================================================ FILE: PyPtt/lib_util.py ================================================ import functools import os import random import re import string import time import traceback from typing import Tuple import requests from . import __version__ from . import check_value from . import data_type from . import i18n from . import log def get_file_name(path_str: str) -> str: result = os.path.basename(path_str) result = result[:result.find('.')] return result def get_current_func_name() -> str: return traceback.extract_stack(None, 2)[0][2] def findnth(haystack, needle, n) -> int: parts = haystack.split(needle, n + 1) if len(parts) <= n + 1: return -1 return len(haystack) - len(parts[-1]) - len(needle) def get_random_str(length) -> str: return ''.join(random.choices(string.hexdigits, k=length)) # 演算法參考 https://www.ptt.cc/man/C_Chat/DE98/DFF5/DB61/M.1419434423.A.DF0.html # aid 字元表 aid_table = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_' def get_aid_from_url(url: str) -> Tuple[str, str]: # 檢查是否為字串 check_value.check_type(url, str, 'url') # 檢查是否符合 PTT BOARD 文章網址格式 pattern = re.compile('https://www.ptt.cc/bbs/[-.\w]+/M.[\d]+.A[.\w]*.html') r = pattern.search(url) if r is None: raise ValueError('wrong parameter url must be www.ptt.cc post url') board = url[23:] board = board[:board.find('/')] temp = url[url.rfind('/') + 1:].split('.') # print(temp) id_0 = int(temp[1]) # dec aid_0 = '' for _ in range(6): index = id_0 % 64 aid_0 = f'{aid_table[index]}{aid_0}' id_0 = int(id_0 / 64) if temp[3] != 'html': id_1 = int(temp[3], 16) # hex aid_1 = '' for _ in range(2): index = id_1 % 64 aid_1 = f'{aid_table[index]}{aid_1}' id_1 = int(id_1 / 64) else: aid_1 = '00' aid = f'{aid_0}{aid_1}' return board, aid sync_version_compare: data_type.Compare = data_type.Compare.UNKNOWN sync_version_result: str = '' def sync_version() -> Tuple[data_type.Compare, str]: global sync_version_compare global sync_version_result if sync_version_compare is not data_type.Compare.UNKNOWN: return sync_version_compare, sync_version_result log.logger.info(i18n.update_remote_version) r = None for i in range(3): try: r = requests.get( 'https://raw.githubusercontent.com/PyPtt/PyPtt/master/PyPtt/__init__.py', timeout=3) break except requests.exceptions.ReadTimeout: log.logger.info(i18n.retry) time.sleep(0.5) if r is None: log.logger.info(i18n.update_remote_version, i18n.fail) return data_type.Compare.SAME, '' log.logger.info(i18n.update_remote_version, i18n.success) text = r.text remote_version = [line for line in text.split('\n') if line.startswith('__version__')][0] remote_version = remote_version[remote_version.find("'") + 1:] remote_version = remote_version[:remote_version.find("'")] current_version = __version__ if 'dev' in current_version: current_version = current_version[:current_version.find('dev') - 1] version_list = [int(v) for v in current_version.split('.')] remote_version_list = [int(v) for v in remote_version.split('.')] sync_version_compare = data_type.Compare.SAME for i in range(len(version_list)): if remote_version_list[i] < version_list[i]: sync_version_compare = data_type.Compare.BIGGER break if version_list[i] < remote_version_list[i]: sync_version_compare = data_type.Compare.SMALLER break return sync_version_compare, remote_version def uniform_new_line(text: str) -> str: random_tag = get_random_str(10) text = text.replace('\r\n', random_tag) text = text.replace('\n', '\r\n') text = text.replace(random_tag, '\r\n') return text @functools.lru_cache(maxsize=64) def check_aid(aid: str) -> str: if aid is None: raise ValueError('aid is None') if not isinstance(aid, str): raise TypeError('aid is not str') if aid.startswith('#'): aid = aid[1:] if len(aid) != 8: raise ValueError('aid is not valid') # check the char of aid is in aid_table or not for char in aid: if char not in aid_table: raise ValueError('aid is not valid') return f'#{aid}' if __name__ == '__main__': check_aid('#1aBzRW4z') ================================================ FILE: PyPtt/log.py ================================================ import logging from typing import Optional class LogLv: _level: int def __init__(self, level): self._level = level @property def level(self): return self._level def __eq__(self, other): return self.level == other.level SILENT = LogLv(logging.NOTSET) INFO = LogLv(logging.INFO) DEBUG = LogLv(logging.DEBUG) # deprecated use DEBUG instead TRACE = DEBUG class LogLevel: SILENT = LogLv(logging.NOTSET) INFO = LogLv(logging.INFO) DEBUG = LogLv(logging.DEBUG) TRACE = DEBUG _logger_pool = {} _console_handler = logging.StreamHandler() _console_handler.setFormatter(logging.Formatter( fmt='[%(asctime)s][%(name)s][%(levelname)s] %(message)s', datefmt='%m.%d %H:%M:%S')) def _combine_msg(*args) -> str: """ 將多個字串組合成一個字串。 Args: args: 要組合的字串。 Returns: 組合後的字串。 """ if not args: return '' msg = list(map(str, args)) msg[0] = msg[0][0].upper() + msg[0][1:] return ' '.join(msg) class Logger: logger: logging.Logger def __init__(self, name: str, level: int = logging.NOTSET, logger_callback: Optional[callable] = None): self.logger = logging.getLogger(name) self.logger.setLevel(level) if self.logger.hasHandlers(): for handler in self.logger.handlers: handler.setFormatter(_console_handler.formatter) else: self.logger.addHandler(_console_handler) self.logger_callback: Optional[callable] = None if logger_callback and callable(logger_callback): self.logger_callback = logger_callback def info(self, *args): if not self.logger.isEnabledFor(logging.INFO): return msg = _combine_msg(*args) self.logger.info(msg) if self.logger_callback: self.logger_callback(msg) def debug(self, *args): if not self.logger.isEnabledFor(logging.DEBUG): return msg = _combine_msg(*args) self.logger.debug(msg) if self.logger_callback: self.logger_callback(msg) logger: Optional[Logger] = None def init(log_level: LogLv, name: Optional[str] = None, logger_callback: Optional[callable] = None) -> Logger: name = name or 'PyPtt' current_logger = Logger(name, level=log_level.level, logger_callback=logger_callback) if name == 'PyPtt': global logger logger = current_logger return current_logger if __name__ == '__main__': logger = init(INFO) logger.info('1') logger.info('1', '2') logger.info('1', '2', '3') logger.debug('debug 1') logger.debug('1', '2') logger.debug('1', '2', '3') logger = init(DEBUG) logger.info('1') logger.info('1', '2') logger.info('1', '2', '3') logger.debug('debug 2') logger.debug('1', '2') logger.debug('1', '2', '3') ================================================ FILE: PyPtt/screens.py ================================================ import re import sys from uao import register_uao from . import log register_uao() class Target: MainMenu = [ '離開,再見', '人, 我是', '[呼叫器]', ] MainMenu_Exiting = [ '【主功能表】', '您確定要離開', ] PTT1_QueryPost = [ '請按任意鍵繼續', '文章代碼(AID):', '文章網址:' ] PTT2_QueryPost = [ '請按任意鍵繼續', '文章代碼(AID):' ] InBoard = [ '看板資訊/設定', '文章選讀', '相關主題' ] InBoardWithCursor = [ '【', '看板資訊/設定', ] InBoardWithCursorLen = len(InBoardWithCursor) # (h)說明 (←/q)離開 # (y)回應(X%)推文(h)說明(←)離開 # (y)回應(X/%)推文 (←)離開 InPost = [ '瀏覽', '頁', ')離開' ] PostEnd = [ '瀏覽', '頁 (100%)', ')離開' ] InWaterBallList = [ '瀏覽', '頁', '說明', ] WaterBallListEnd = [ '瀏覽', '頁 (100%)', '說明' ] PostIP_New = [ '※ 發信站: 批踢踢實業坊(ptt.cc), 來自:' ] PostIP_Old = [ '◆ From:' ] Edit = [ '※ 編輯' ] PostURL = [ '※ 文章網址' ] Vote_Type1 = [ '◆ 投票名稱', '◆ 投票中止於', '◆ 票選題目描述' ] Vote_Type2 = [ '投票名稱', '◆ 預知投票紀事', ] AnyKey = '任意鍵' InTalk = [ '【聊天說話】', '線上使用者列表', '查詢網友', '顯示上幾次熱訊' ] InUserList = [ '休閒聊天', '聊天/寫信', '說明', ] InMailBox = [ '【郵件選單】', '[~]資源回收筒', '鴻雁往返' ] InMailBoxWithCursor = [ '【郵件選單】', '[~]資源回收筒', ] InMailBoxWithCursorLen = len(InMailBoxWithCursor) InMailMenu = [ '【電子郵件】', '我的信箱', '把所有私人資料打包回去', '寄信給帳號站長', ] PostNoContent = [ '◆ 此文章無內容', AnyKey ] InBoardList = [ '【看板列表】', '選擇看板', '只列最愛', '已讀/未讀' ] use_too_many_resources = [ '程式耗用過多計算資源' ] Animation = [ '★ 這份文件是可播放的文字動畫,要開始播放嗎?' ] CursorToGoodbye = MainMenu.copy() content_start = '─── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ──' content_end_list = [ '--\n※ 發信站: 批踢踢實業坊', '--\n※ 發信站: 批踢踢兔(ptt2.cc)', '--\n※ 發信站: 新批踢踢(ptt2.twbbs.org.tw)' ] def show(config, screen_queue, function_name=None): if config.log_level != log.DEBUG: return if isinstance(screen_queue, list): for Screen in screen_queue: print('-' * 50) try: print( Screen.encode( sys.stdin.encoding, "replace").decode( sys.stdin.encoding)) except Exception: print(Screen.encode('utf-8', "replace").decode('utf-8')) else: print('-' * 50) try: print(screen_queue.encode( sys.stdin.encoding, "replace").decode( sys.stdin.encoding)) except Exception: print(screen_queue.encode('utf-8', "replace").decode('utf-8')) print('len:' + str(len(screen_queue))) if function_name is not None: print('錯誤在 ' + function_name + ' 函式發生') print('-' * 50) xy_pattern_h = re.compile('^=ESC=\[[\d]+;[\d]+H') xy_pattern_s = re.compile('^=ESC=\[[\d]+;[\d]+s') class VT100Parser: def _h(self): self._cursor_x = 0 self._cursor_y = 0 def _2j(self): self.screen = [''] * 24 self.screen_length = dict() def _move(self, x, y): self._cursor_x = x self._cursor_y = y def _newline(self): self._cursor_x = 0 self._cursor_y += 1 def _k(self): if self._cursor_x == 0: # nothing happen but cause error return self.screen[self._cursor_y] = self.screen[self._cursor_y][:self._cursor_x] def __init__(self, bytes_data, encoding): # self._data = data # https://www.csie.ntu.edu.tw/~r88009/Java/html/Network/vt100.htm self._cursor_x = 0 self._cursor_y = 0 self.screen = [''] * 24 self.screen_length = dict() data = bytes_data.decode(encoding, errors='replace') # remove color data = re.sub('\x1B\[[\d+;]*m', '', data) data = re.sub(r'[\x1B]', '=ESC=', data) data = re.sub(r'[\r]', '', data) while ' \x08' in data: data = re.sub(r' \x08', '', data) # print('---' * 8) # print(encoding) # print(bytes_data) # print(data) # print('---' * 8) if '=ESC=[2J' in data: data = data[data.rfind('=ESC=[2J') + len('=ESC=[2J'):] count = 0 while data: count += 1 while True: if not data.startswith('=ESC='): break if data.startswith('=ESC=[H'): data = data[len('=ESC=[H'):] self._h() continue elif data.startswith('=ESC=[K'): data = data[len('=ESC=[K'):] self._k() continue elif data.startswith('=ESC=[s'): data = data[len('=ESC=[s'):] continue break xy_result = None xy_result_h = xy_pattern_h.search(data) if not xy_result_h: xy_result_s = xy_pattern_s.search(data) if xy_result_s: xy_result = xy_result_s else: xy_result = xy_result_h if xy_result: xy_part = xy_result.group(0) new_y = int(xy_part[6:xy_part.find(';')]) - 1 new_x = int(xy_part[xy_part.find(';') + 1: -1]) # log.py.info('xy', xy_part, new_x, new_y) self._move(new_x, new_y) data = data[len(xy_part):] else: if data.startswith('\n'): data = data[1:] self._newline() continue # print(f'-{data[:1]}-{len(data[:1].encode("big5-uao", "replace"))}') if self._cursor_y not in self.screen_length: self.screen_length[self._cursor_y] = len(self.screen[self._cursor_y].encode(encoding, 'replace')) current_line_length = self.screen_length[self._cursor_y] replace_mode = False if current_line_length < self._cursor_x: append_space = ' ' * (self._cursor_x - current_line_length) self.screen[self._cursor_y] += append_space elif current_line_length > self._cursor_x: replace_mode = True next_newline = data.find('\n') next_newline = 1920 if next_newline < 0 else next_newline next_esc = data.find('=ESC=') next_esc = 1920 if next_esc < 0 else next_esc if next_esc == 0: break current_index = min(next_newline, next_esc) current_data = data[:current_index] current_data_length = len(current_data.encode(encoding, 'replace')) # print('=', current_data, '=', current_data_length) if replace_mode: current_line = self.screen[self._cursor_y][:self._cursor_x] current_line += current_data current_line += self.screen[self._cursor_y][self._cursor_x + len(current_data):] self.screen[self._cursor_y] = current_line else: self.screen[self._cursor_y] += current_data self._cursor_x += current_data_length self.screen_length[self._cursor_y] = self._cursor_x data = data[current_index:] # print('\n'.join(self.screen)) # print('\n'.join(self._screen)) # print('=' * 20) # print(data) # print('Spend', count, 'cycle') self.screen = '\n'.join(self.screen) if __name__ == '__main__': # post list screen = b"\x1b[H\x1b[2J\x1b[1;37;44m\xe3\x80\x90\xe4\xb8\xbb\xe5\x8a\x9f\xe8\x83\xbd\xe8\xa1\xa8\xe3\x80\x91 \x1b[33m\xe6\x89\xb9\xe8\xb8\xa2\xe8\xb8\xa2\xe5\xaf\xa6\xe6\xa5\xad\xe5\x9d\x8a\x1b[0;1;37;44m \r\n \x08\x08\x1b[47m\xe2\x96\x88\x1b[2;3H \x08\x08\xe2\x96\x88\x1b[2;5H \x08\x08\xe2\x96\x88\x1b[2;7H \x08\x08\xe2\x96\x88\x1b[2;9H \x08\x08\xe2\x96\x88\x1b[2;11H \x08\x08\xe2\x96\x88\x1b[2;13H \x08\x08\xe2\x96\x88\x1b[2;15H \x08\x08\xe2\x96\x88\x1b[2;17H \x08\x08\xe2\x96\x88\x1b[2;19H \x08\x08\xe2\x96\x88\x1b[2;21H \x08\x08\xe2\x96\x88\x1b[2;23H \x08\x08\xe2\x96\x88\x1b[2;25H \x08\x08\xe2\x96\x88\x1b[2;27H \x08\x08\xe2\x96\x88\x1b[2;29H \x08\x08\xe2\x96\x88\x1b[2;31H \x08\x08\xe2\x96\x88\x1b[2;33H \x08\x08\xe2\x96\x88\x1b[2;35H \x08\x08\xe2\x96\x88\x1b[2;37H \x08\x08\xe2\x96\x88\x1b[2;39H \x08\x08\xe2\x96\x88\x1b[2;41H \x08\x08\xe2\x96\x88\x1b[2;43H \x08\x08\xe2\x96\x88\x1b[2;45H \x08\x08\xe2\x96\x88\x1b[2;47H \x08\x08\xe2\x96\x88\x1b[2;49H \x08\x08\xe2\x96\x88\x1b[2;51H \x08\x08\xe2\x96\x88\x1b[2;53H \x08\x08\xe2\x96\x88\x1b[2;55H \x08\x08\xe2\x96\x88\x1b[2;57H \x08\x08\xe2\x96\x88\x1b[2;59H \x08\x08\xe2\x96\x88\x1b[2;61H \x08\x08\xe2\x96\x88\x1b[2;63H \x08\x08\xe2\x96\x88\x1b[2;65H \x08\x08\xe2\x96\x88\x1b[2;67H \x08\x08\xe2\x96\x88\x1b[2;69H \x08\x08\xe2\x96\x88\x1b[2;71H \x08\x08\xe2\x96\x88\x1b[2;73H \x08\x08\xe2\x96\x88\x1b[2;75H \x08\x08\xe2\x96\x88\x1b[2;77H \x08\x08\xe2\x96\x88\x1b[2;79H\r\n \x08\x08\x1b[0;33;47m\xe2\x96\x85\x1b[3;3H \x08\x08\xe2\x96\x86\x1b[3;5H \x08\x08\xe2\x96\x86\x1b[3;7H \x08\x08\x1b[1;36;43m\xe2\x97\xa5\x1b[3;9H \x08\x08\x1b[47m\xe2\x96\x88\x1b[3;11H \x08\x08\xe2\x96\x88\x1b[3;13H \x08\x08\xe2\x96\x88\x1b[3;15H \x08\x08\xe2\x96\x88\x1b[3;17H \x08\x08\xe2\x96\x88\x1b[3;19H \x08\x08\xe2\x96\x88\x1b[3;21H \x08\x08\xe2\x96\x88\x1b[3;23H \x08\x08\xe2\x96\x88\x1b[3;25H \x08\x08\xe2\x96\x88\x1b[3;27H \x08\x08\xe2\x96\x88\x1b[3;29H \x08\x08\xe2\x96\x88\x1b[3;31H \x08\x08\xe2\x96\x88\x1b[3;33H \x08\x08\xe2\x96\x88\x1b[3;35H \x08\x08\xe2\x96\x88\x1b[3;37H \x08\x08\xe2\x96\x88\x1b[3;39H \x08\x08\xe2\x96\x88\x1b[3;41H \x08\x08\xe2\x96\x88\x1b[3;43H \x08\x08\xe2\x96\x88\x1b[3;45H \x08\x08\xe2\x96\x88\x1b[3;47H \x08\x08\xe2\x96\x88\x1b[3;49H \x08\x08\xe2\x96\x88\x1b[3;51H \x08\x08\xe2\x96\x88\x1b[3;53H \x08\x08\xe2\x96\x88\x1b[3;55H \x08\x08\xe2\x96\x88\x1b[3;57H \x08\x08\xe2\x96\x88\x1b[3;59H \x08\x08\xe2\x96\x88\x1b[3;61H \x08\x08\xe2\x96\x88\x1b[3;63H \x08\x08\xe2\x96\x88\x1b[3;65H \x08\x08\xe2\x96\x88\x1b[3;67H \x08\x08\xe2\x96\x88\x1b[3;69H \x08\x08\xe2\x96\x88\x1b[3;71H \x08\x08\xe2\x96\x88\x1b[3;73H \x08\x08\xe2\x96\x88\x1b[3;75H \x08\x08\xe2\x96\x88\x1b[3;77H \x08\x08\xe2\x96\x88\x1b[3;79H\r\n\x1b[0;30;43m - \x08\x08\xe2\x94\x80\x1b[4;9H` \x1b[1;36m\xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3\x1b[30m\xef\xbf\xa3\xef\xbf\xa3\x1b[36m\xef\xbf\xa3 \r\n\x1b[0;43m \x08\x08\x1b[31m\xe2\x96\x82\x1b[5;15H \x08\x08\xe2\x96\x83\x1b[5;17H\x1b[1;30m \xef\xb8\xb1 \xef\xbd\x9c \xef\xb9\xa8 \x1b[0;30;43m\xef\xbd\x9c \\ \x08\x08\xe2\x88\xa3\x1b[5;77H \r\n \\_ \x08\x08\xe2\x88\x95\x1b[6;9H \x08\x08\x1b[33;41m\xe2\x97\xa4\x1b[6;12H \x08\x08\x1b[0;41m\xe2\x96\x82\x1b[6;14H \x08\x08\x1b[31;43m\xe2\x96\x8a\x1b[6;17H\x1b[34m\xe3\x80\x82\x1b[30m / \x08\x08\xe2\x88\x95\x1b[6;27H \xef\xb9\x8d \xef\xb9\xa8 \x1b[1mL_ \x1b[0;30;43m| | \x08\x08\xe2\x8a\xa5\x1b[6;61H \x1b[1m\xef\xbd\x9c \x1b[0;30;43m. \x08\x08\xe2\x88\xa0\x1b[6;76H \r\n \x08\x08\xe2\x95\xb3\x1b[7;6H \x08\x08\x1b[31;47m\xe2\x96\x8a\x1b[7;12H \x08\x08\x1b[30m\xcb\x99\x1b[7;14H \x08\x08\x1b[0;41m\xe2\x96\x8e\x1b[7;16H \x08\x08\x1b[33m\xe2\x97\xa4\x1b[7;18H \x08\x08\x1b[31;43m\xe2\x96\x8e\x1b[7;20H\x1b[30m` \x08\x08\xe2\x94\x90\x1b[7;23H \x08\x08\xe2\x94\x82\x1b[7;26H, \xef\xb8\xb3 \x08\x08\xe2\x96\x81\x1b[7;34H \x08\x08\xe2\x95\x93\x1b[7;39H-.\xef\xb8\xbf \x08\x08\xe2\x88\x95\x1b[7;48H ` _\xef\xb8\xb3 \x08\x08\xe2\x88\xa3\x1b[7;58H . _x_ \\ \xef\xbd\x9c \x08\x08\xe2\x95\xb2\x1b[7;78H \r\n L \x1b[36m_ \x08\x08\xe2\x96\x84\x1b[8;8H \x08\x08\x1b[41m\xe2\x96\x8a\x1b[8;10H\x1b[30m\\ \x08\x08\x1b[31;47m\xe2\x96\x86\x1b[8;14H\x1b[1;36;41m' \x1b[0;30;43m N \xef\xbc\xbc_7_\xef\xb8\xb7-+ \x08\x08\xe2\x94\xa4\x1b[8;33H \x08\x08\xe2\x86\x96\x1b[8;36H \x08\x08\xe2\x95\xb3\x1b[8;38H \xe3\x80\x89 \x08\x08\xe2\x94\xac\x1b[8;45H' \x08\x08\xe2\x94\x8c\x1b[8;48H \x08\x08\xe2\x80\xb5\x1b[8;50H \x08\x08\xe2\x88\x9a\x1b[8;52H 7\xe2\x95\xb4.-\xef\xbc\x81 \x08\x08\xe2\x80\xb2\x1b[8;62H ` \x08\x08\xe2\x94\xac\x1b[8;68H+ \x08\x08\xe2\x94\xbc\x1b[8;71H=. \x08\x08\xe2\x88\x95\x1b[8;75H= \xe2\x95\xb4\r\n \x08\x08\xe2\x94\xa4\x1b[9;3H \x08\x08\x1b[33;46m\xe2\x96\x84\x1b[9;6H \x08\x08\x1b[31m\xe2\x96\x84\x1b[9;9H\x1b[30;41m, \x08\x08\xe2\x95\xb2\x1b[9;13H,_ \x08\x08\x1b[33m\xe2\x97\xa2\x1b[9;19H \x08\x08\x1b[30;43m\xe2\x94\x82\x1b[9;21H\\_ \x08\x08\xe2\x95\xb1\x1b[9;25H \x08\x08\xe2\x96\x8f\x1b[9;27H\\_ \x08\x08\xe2\x88\x95\x1b[9;31H_\xef\xb9\x80 _;} \x08\x08\xe2\x80\x94\x1b[9;41HL \x08\x08\xe2\x86\x98\x1b[9;46H_\xe3\x80\x95-_ \x08\x08\xe2\x95\xb3\x1b[9;53H | \xef\xbc\xbc__F \x08\x08\xe2\x86\x99\x1b[9;67H \xef\xb9\x8d] \x08\x08\xe2\x96\x8f\x1b[9;75H_ \x08\x08\xe2\x88\x95\x1b[9;78H \r\n \x08\x08\xe2\x95\xb2\x1b[10;4H\x1b[32mr \x08\x08\x1b[46m\xe2\x96\x8e\x1b[10;7H \x08\x08\x1b[31m\xe2\x97\xa5\x1b[10;9H\x1b[30;41m\xe3\x80\x83 \x08\x08\xe2\x94\x94\x1b[10;13H\x1b[34m_ \x08\x08\x1b[36m\xe2\x96\x84\x1b[10;16H\x1b[31;43m\xe3\x80\x9e \x08\x08\x1b[30m\xe2\x86\x97\x1b[10;20H \x08\x08\xe2\x95\xb3\x1b[10;22H \x08\x08\xe2\x96\x95\x1b[10;25H_ \x08\x08\xe2\x94\x98\x1b[10;29H \x08\x08\xe2\x95\xb3\x1b[10;31H \x08\x08\xe2\x95\xb2\x1b[10;34H_ \x08\x08\xe2\x86\x99\x1b[10;37H \x08\x08\xe2\x96\x8e\x1b[10;39H \x08\x08\xe2\x86\x99\x1b[10;41H \x08\x08\xe2\x86\x91\x1b[10;43H_ \x08\x08\xe2\x96\x95\x1b[10;46H , \x08\x08\xe2\x94\xbc\x1b[10;51H \x08\x08\xe2\x96\x95\x1b[10;53H \xef\xb9\xa8 \xef\xb9\x80` \x08\x08\xe2\x96\x8e\x1b[10;66H`\xef\xbf\xa3 \x08\x08\xe2\x86\x96\x1b[10;73H\xef\xbf\xa3} \x08\x08\xe2\x95\xb2\x1b[10;79H\r\n r \x08\x08\xe2\x94\x98\x1b[11;5H \x08\x08\x1b[33;46m\xe2\x96\x84\x1b[11;8H \x08\x08\x1b[31m\xe2\x97\xa5\x1b[11;11H \x08\x08\x1b[33m\xe2\x96\x83\x1b[11;13H \x08\x08\xe2\x96\x86\x1b[11;15H \x08\x08\x1b[30;43m\xe2\x94\xac\x1b[11;17H \x08\x08\xe2\x95\x9d\x1b[11;19H \x08\x08\xe2\x94\x94\x1b[11;21H \x08\x08\xe2\x96\x8e\x1b[11;24H \x08\x08\xe2\x95\xb2\x1b[11;28H ` \x08\x08\xe2\x96\x8f\x1b[11;33H \x08\x08\xe2\x94\x9c\x1b[11;35H \xef\xbf\xa3\\ \x08\x08\xe2\x80\x99\x1b[11;44H\xef\xb8\xba \x08\x08\xe2\x96\x8f\x1b[11;48H\x1b[35m\xe5\x8f\xaf\xe4\xb8\x8d\xe5\x8f\xaf\xe4\xbb\xa5\xe5\x81\xb6\xe7\x88\xbe\xe4\xb8\x8b\xe9\x9b\xa8\xe4\xb8\x8d\xe5\xbf\x85\xe6\xb0\xb8\xe9\x81\xa0\xe6\x99\xb4\xe5\xa4\xa9...\x1b[13;23H\x1b[m(\x1b[1;36mA\x1b[m)nnounce \xe3\x80\x90 \xe7\xb2\xbe\xe8\x8f\xaf\xe5\x85\xac\xe4\xbd\x88\xe6\xac\x84 \xe3\x80\x91\x1b[14;23H(\x1b[1;36mF\x1b[m)avorite \xe3\x80\x90 \xe6\x88\x91 \xe7\x9a\x84 \xe6\x9c\x80\xe6\x84\x9b \xe3\x80\x91\x1b[15;21H> (\x1b[1;36mC\x1b[m)lass\x1b[15;38H\xe3\x80\x90 \xe5\x88\x86\xe7\xb5\x84\xe8\xa8\x8e\xe8\xab\x96\xe5\x8d\x80 \xe3\x80\x91\x1b[16;23H(\x1b[1;36mM\x1b[m)ail\x1b[16;38H\xe3\x80\x90 \xe7\xa7\x81\xe4\xba\xba\xe4\xbf\xa1\xe4\xbb\xb6\xe5\x8d\x80 \xe3\x80\x91\x1b[17;23H(\x1b[1;36mT\x1b[m)alk\x1b[17;38H\xe3\x80\x90 \xe4\xbc\x91\xe9\x96\x92\xe8\x81\x8a\xe5\xa4\xa9\xe5\x8d\x80 \xe3\x80\x91\x1b[18;23H(\x1b[1;36mU\x1b[m)ser\x1b[18;38H\xe3\x80\x90 \xe5\x80\x8b\xe4\xba\xba\xe8\xa8\xad\xe5\xae\x9a\xe5\x8d\x80 \xe3\x80\x91\x1b[19;23H(\x1b[1;36mX\x1b[m)yz\x1b[19;38H\xe3\x80\x90 \xe7\xb3\xbb\xe7\xb5\xb1\xe8\xb3\x87\xe8\xa8\x8a\xe5\x8d\x80 \xe3\x80\x91\x1b[20;23H(\x1b[1;36mP\x1b[m)lay\x1b[20;38H\xe3\x80\x90 \xe5\xa8\x9b\xe6\xa8\x82\xe8\x88\x87\xe4\xbc\x91\xe9\x96\x92 \xe3\x80\x91\x1b[21;23H(\x1b[1;36mN\x1b[m)amelist \xe3\x80\x90 \xe7\xb7\xa8\xe7\x89\xb9\xe5\x88\xa5\xe5\x90\x8d\xe5\x96\xae \xe3\x80\x91\x1b[22;23H(\x1b[1;36mG\x1b[m)oodbye\x1b[22;41H\xe9\x9b\xa2\xe9\x96\x8b\xef\xbc\x8c\xe5\x86\x8d\xe8\xa6\x8b \x08\x08\xe2\x80\xa6\x1b[22;53H\r\n\n\x1b[34;46m[12/4 \xe6\x98\x9f\xe6\x9c\x9f\xe5\x85\xad 14:26]\x1b[1;33;45m [ \xe9\x9b\x99\xe9\xad\x9a\xe6\x99\x82 ] \x1b[30;47m \xe7\xb7\x9a\xe4\xb8\x8a\x1b[31m66391\x1b[30m\xe4\xba\xba, \xe6\x88\x91\xe6\x98\xaf\x1b[31mCodingMan\x1b[30m [\xe5\x91\xbc\xe5\x8f\xab\xe5\x99\xa8]\x1b[31m\xe9\x97\x9c\xe9\x96\x89 \x1b[m\x1b[15;21H\x1b[H\x1b[2J\x1b[1;37;44m\xe3\x80\x90\xe4\xb8\xbb\xe5\x8a\x9f\xe8\x83\xbd\xe8\xa1\xa8\xe3\x80\x91 \x1b[33m\xe6\x89\xb9\xe8\xb8\xa2\xe8\xb8\xa2\xe5\xaf\xa6\xe6\xa5\xad\xe5\x9d\x8a\x1b[0;1;37;44m \r\n \x08\x08\x1b[47m\xe2\x96\x88\x1b[2;3H \x08\x08\xe2\x96\x88\x1b[2;5H \x08\x08\xe2\x96\x88\x1b[2;7H \x08\x08\xe2\x96\x88\x1b[2;9H \x08\x08\xe2\x96\x88\x1b[2;11H \x08\x08\xe2\x96\x88\x1b[2;13H \x08\x08\xe2\x96\x88\x1b[2;15H \x08\x08\xe2\x96\x88\x1b[2;17H \x08\x08\xe2\x96\x88\x1b[2;19H \x08\x08\xe2\x96\x88\x1b[2;21H \x08\x08\xe2\x96\x88\x1b[2;23H \x08\x08\xe2\x96\x88\x1b[2;25H \x08\x08\xe2\x96\x88\x1b[2;27H \x08\x08\xe2\x96\x88\x1b[2;29H \x08\x08\xe2\x96\x88\x1b[2;31H \x08\x08\xe2\x96\x88\x1b[2;33H \x08\x08\xe2\x96\x88\x1b[2;35H \x08\x08\xe2\x96\x88\x1b[2;37H \x08\x08\xe2\x96\x88\x1b[2;39H \x08\x08\xe2\x96\x88\x1b[2;41H \x08\x08\xe2\x96\x88\x1b[2;43H \x08\x08\xe2\x96\x88\x1b[2;45H \x08\x08\xe2\x96\x88\x1b[2;47H \x08\x08\xe2\x96\x88\x1b[2;49H \x08\x08\xe2\x96\x88\x1b[2;51H \x08\x08\xe2\x96\x88\x1b[2;53H \x08\x08\xe2\x96\x88\x1b[2;55H \x08\x08\xe2\x96\x88\x1b[2;57H \x08\x08\xe2\x96\x88\x1b[2;59H \x08\x08\xe2\x96\x88\x1b[2;61H \x08\x08\xe2\x96\x88\x1b[2;63H \x08\x08\xe2\x96\x88\x1b[2;65H \x08\x08\xe2\x96\x88\x1b[2;67H \x08\x08\xe2\x96\x88\x1b[2;69H \x08\x08\xe2\x96\x88\x1b[2;71H \x08\x08\xe2\x96\x88\x1b[2;73H \x08\x08\xe2\x96\x88\x1b[2;75H \x08\x08\xe2\x96\x88\x1b[2;77H \x08\x08\xe2\x96\x88\x1b[2;79H\r\n \x08\x08\x1b[0;33;47m\xe2\x96\x85\x1b[3;3H \x08\x08\xe2\x96\x86\x1b[3;5H \x08\x08\xe2\x96\x86\x1b[3;7H \x08\x08\x1b[1;36;43m\xe2\x97\xa5\x1b[3;9H \x08\x08\x1b[47m\xe2\x96\x88\x1b[3;11H \x08\x08\xe2\x96\x88\x1b[3;13H \x08\x08\xe2\x96\x88\x1b[3;15H \x08\x08\xe2\x96\x88\x1b[3;17H \x08\x08\xe2\x96\x88\x1b[3;19H \x08\x08\xe2\x96\x88\x1b[3;21H \x08\x08\xe2\x96\x88\x1b[3;23H \x08\x08\xe2\x96\x88\x1b[3;25H \x08\x08\xe2\x96\x88\x1b[3;27H \x08\x08\xe2\x96\x88\x1b[3;29H \x08\x08\xe2\x96\x88\x1b[3;31H \x08\x08\xe2\x96\x88\x1b[3;33H \x08\x08\xe2\x96\x88\x1b[3;35H \x08\x08\xe2\x96\x88\x1b[3;37H \x08\x08\xe2\x96\x88\x1b[3;39H \x08\x08\xe2\x96\x88\x1b[3;41H \x08\x08\xe2\x96\x88\x1b[3;43H \x08\x08\xe2\x96\x88\x1b[3;45H \x08\x08\xe2\x96\x88\x1b[3;47H \x08\x08\xe2\x96\x88\x1b[3;49H \x08\x08\xe2\x96\x88\x1b[3;51H \x08\x08\xe2\x96\x88\x1b[3;53H \x08\x08\xe2\x96\x88\x1b[3;55H \x08\x08\xe2\x96\x88\x1b[3;57H \x08\x08\xe2\x96\x88\x1b[3;59H \x08\x08\xe2\x96\x88\x1b[3;61H \x08\x08\xe2\x96\x88\x1b[3;63H \x08\x08\xe2\x96\x88\x1b[3;65H \x08\x08\xe2\x96\x88\x1b[3;67H \x08\x08\xe2\x96\x88\x1b[3;69H \x08\x08\xe2\x96\x88\x1b[3;71H \x08\x08\xe2\x96\x88\x1b[3;73H \x08\x08\xe2\x96\x88\x1b[3;75H \x08\x08\xe2\x96\x88\x1b[3;77H \x08\x08\xe2\x96\x88\x1b[3;79H\r\n\x1b[0;30;43m - \x08\x08\xe2\x94\x80\x1b[4;9H` \x1b[1;36m\xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3\x1b[30m\xef\xbf\xa3\xef\xbf\xa3\x1b[36m\xef\xbf\xa3 \r\n\x1b[0;43m \x08\x08\x1b[31m\xe2\x96\x82\x1b[5;15H \x08\x08\xe2\x96\x83\x1b[5;17H\x1b[1;30m \xef\xb8\xb1 \xef\xbd\x9c \xef\xb9\xa8 \x1b[0;30;43m\xef\xbd\x9c \\ \x08\x08\xe2\x88\xa3\x1b[5;77H \r\n \\_ \x08\x08\xe2\x88\x95\x1b[6;9H \x08\x08\x1b[33;41m\xe2\x97\xa4\x1b[6;12H \x08\x08\x1b[0;41m\xe2\x96\x82\x1b[6;14H \x08\x08\x1b[31;43m\xe2\x96\x8a\x1b[6;17H\x1b[34m\xe3\x80\x82\x1b[30m / \x08\x08\xe2\x88\x95\x1b[6;27H \xef\xb9\x8d \xef\xb9\xa8 \x1b[1mL_ \x1b[0;30;43m| | \x08\x08\xe2\x8a\xa5\x1b[6;61H \x1b[1m\xef\xbd\x9c \x1b[0;30;43m. \x08\x08\xe2\x88\xa0\x1b[6;76H \r\n \x08\x08\xe2\x95\xb3\x1b[7;6H \x08\x08\x1b[31;47m\xe2\x96\x8a\x1b[7;12H \x08\x08\x1b[30m\xcb\x99\x1b[7;14H \x08\x08\x1b[0;41m\xe2\x96\x8e\x1b[7;16H \x08\x08\x1b[33m\xe2\x97\xa4\x1b[7;18H \x08\x08\x1b[31;43m\xe2\x96\x8e\x1b[7;20H\x1b[30m` \x08\x08\xe2\x94\x90\x1b[7;23H \x08\x08\xe2\x94\x82\x1b[7;26H, \xef\xb8\xb3 \x08\x08\xe2\x96\x81\x1b[7;34H \x08\x08\xe2\x95\x93\x1b[7;39H-.\xef\xb8\xbf \x08\x08\xe2\x88\x95\x1b[7;48H ` _\xef\xb8\xb3 \x08\x08\xe2\x88\xa3\x1b[7;58H . _x_ \\ \xef\xbd\x9c \x08\x08\xe2\x95\xb2\x1b[7;78H \r\n L \x1b[36m_ \x08\x08\xe2\x96\x84\x1b[8;8H \x08\x08\x1b[41m\xe2\x96\x8a\x1b[8;10H\x1b[30m\\ \x08\x08\x1b[31;47m\xe2\x96\x86\x1b[8;14H\x1b[1;36;41m' \x1b[0;30;43m N \xef\xbc\xbc_7_\xef\xb8\xb7-+ \x08\x08\xe2\x94\xa4\x1b[8;33H \x08\x08\xe2\x86\x96\x1b[8;36H \x08\x08\xe2\x95\xb3\x1b[8;38H \xe3\x80\x89 \x08\x08\xe2\x94\xac\x1b[8;45H' \x08\x08\xe2\x94\x8c\x1b[8;48H \x08\x08\xe2\x80\xb5\x1b[8;50H \x08\x08\xe2\x88\x9a\x1b[8;52H 7\xe2\x95\xb4.-\xef\xbc\x81 \x08\x08\xe2\x80\xb2\x1b[8;62H ` \x08\x08\xe2\x94\xac\x1b[8;68H+ \x08\x08\xe2\x94\xbc\x1b[8;71H=. \x08\x08\xe2\x88\x95\x1b[8;75H= \xe2\x95\xb4\r\n \x08\x08\xe2\x94\xa4\x1b[9;3H \x08\x08\x1b[33;46m\xe2\x96\x84\x1b[9;6H \x08\x08\x1b[31m\xe2\x96\x84\x1b[9;9H\x1b[30;41m, \x08\x08\xe2\x95\xb2\x1b[9;13H,_ \x08\x08\x1b[33m\xe2\x97\xa2\x1b[9;19H \x08\x08\x1b[30;43m\xe2\x94\x82\x1b[9;21H\\_ \x08\x08\xe2\x95\xb1\x1b[9;25H \x08\x08\xe2\x96\x8f\x1b[9;27H\\_ \x08\x08\xe2\x88\x95\x1b[9;31H_\xef\xb9\x80 _;} \x08\x08\xe2\x80\x94\x1b[9;41HL \x08\x08\xe2\x86\x98\x1b[9;46H_\xe3\x80\x95-_ \x08\x08\xe2\x95\xb3\x1b[9;53H | \xef\xbc\xbc__F \x08\x08\xe2\x86\x99\x1b[9;67H \xef\xb9\x8d] \x08\x08\xe2\x96\x8f\x1b[9;75H_ \x08\x08\xe2\x88\x95\x1b[9;78H \r\n \x08\x08\xe2\x95\xb2\x1b[10;4H\x1b[32mr \x08\x08\x1b[46m\xe2\x96\x8e\x1b[10;7H \x08\x08\x1b[31m\xe2\x97\xa5\x1b[10;9H\x1b[30;41m\xe3\x80\x83 \x08\x08\xe2\x94\x94\x1b[10;13H\x1b[34m_ \x08\x08\x1b[36m\xe2\x96\x84\x1b[10;16H\x1b[31;43m\xe3\x80\x9e \x08\x08\x1b[30m\xe2\x86\x97\x1b[10;20H \x08\x08\xe2\x95\xb3\x1b[10;22H \x08\x08\xe2\x96\x95\x1b[10;25H_ \x08\x08\xe2\x94\x98\x1b[10;29H \x08\x08\xe2\x95\xb3\x1b[10;31H \x08\x08\xe2\x95\xb2\x1b[10;34H_ \x08\x08\xe2\x86\x99\x1b[10;37H \x08\x08\xe2\x96\x8e\x1b[10;39H \x08\x08\xe2\x86\x99\x1b[10;41H \x08\x08\xe2\x86\x91\x1b[10;43H_ \x08\x08\xe2\x96\x95\x1b[10;46H , \x08\x08\xe2\x94\xbc\x1b[10;51H \x08\x08\xe2\x96\x95\x1b[10;53H \xef\xb9\xa8 \xef\xb9\x80` \x08\x08\xe2\x96\x8e\x1b[10;66H`\xef\xbf\xa3 \x08\x08\xe2\x86\x96\x1b[10;73H\xef\xbf\xa3} \x08\x08\xe2\x95\xb2\x1b[10;79H\r\n r \x08\x08\xe2\x94\x98\x1b[11;5H \x08\x08\x1b[33;46m\xe2\x96\x84\x1b[11;8H \x08\x08\x1b[31m\xe2\x97\xa5\x1b[11;11H \x08\x08\x1b[33m\xe2\x96\x83\x1b[11;13H \x08\x08\xe2\x96\x86\x1b[11;15H \x08\x08\x1b[30;43m\xe2\x94\xac\x1b[11;17H \x08\x08\xe2\x95\x9d\x1b[11;19H \x08\x08\xe2\x94\x94\x1b[11;21H \x08\x08\xe2\x96\x8e\x1b[11;24H \x08\x08\xe2\x95\xb2\x1b[11;28H ` \x08\x08\xe2\x96\x8f\x1b[11;33H \x08\x08\xe2\x94\x9c\x1b[11;35H \xef\xbf\xa3\\ \x08\x08\xe2\x80\x99\x1b[11;44H\xef\xb8\xba \x08\x08\xe2\x96\x8f\x1b[11;48H\x1b[35m\xe5\x8f\xaf\xe4\xb8\x8d\xe5\x8f\xaf\xe4\xbb\xa5\xe5\x81\xb6\xe7\x88\xbe\xe4\xb8\x8b\xe9\x9b\xa8\xe4\xb8\x8d\xe5\xbf\x85\xe6\xb0\xb8\xe9\x81\xa0\xe6\x99\xb4\xe5\xa4\xa9...\x1b[13;23H\x1b[m(\x1b[1;36mA\x1b[m)nnounce \xe3\x80\x90 \xe7\xb2\xbe\xe8\x8f\xaf\xe5\x85\xac\xe4\xbd\x88\xe6\xac\x84 \xe3\x80\x91\x1b[14;23H(\x1b[1;36mF\x1b[m)avorite \xe3\x80\x90 \xe6\x88\x91 \xe7\x9a\x84 \xe6\x9c\x80\xe6\x84\x9b \xe3\x80\x91\x1b[15;21H> (\x1b[1;36mC\x1b[m)lass\x1b[15;38H\xe3\x80\x90 \xe5\x88\x86\xe7\xb5\x84\xe8\xa8\x8e\xe8\xab\x96\xe5\x8d\x80 \xe3\x80\x91\x1b[16;23H(\x1b[1;36mM\x1b[m)ail\x1b[16;38H\xe3\x80\x90 \xe7\xa7\x81\xe4\xba\xba\xe4\xbf\xa1\xe4\xbb\xb6\xe5\x8d\x80 \xe3\x80\x91\x1b[17;23H(\x1b[1;36mT\x1b[m)alk\x1b[17;38H\xe3\x80\x90 \xe4\xbc\x91\xe9\x96\x92\xe8\x81\x8a\xe5\xa4\xa9\xe5\x8d\x80 \xe3\x80\x91\x1b[18;23H(\x1b[1;36mU\x1b[m)ser\x1b[18;38H\xe3\x80\x90 \xe5\x80\x8b\xe4\xba\xba\xe8\xa8\xad\xe5\xae\x9a\xe5\x8d\x80 \xe3\x80\x91\x1b[19;23H(\x1b[1;36mX\x1b[m)yz\x1b[19;38H\xe3\x80\x90 \xe7\xb3\xbb\xe7\xb5\xb1\xe8\xb3\x87\xe8\xa8\x8a\xe5\x8d\x80 \xe3\x80\x91\x1b[20;23H(\x1b[1;36mP\x1b[m)lay\x1b[20;38H\xe3\x80\x90 \xe5\xa8\x9b\xe6\xa8\x82\xe8\x88\x87\xe4\xbc\x91\xe9\x96\x92 \xe3\x80\x91\x1b[21;23H(\x1b[1;36mN\x1b[m)amelist \xe3\x80\x90 \xe7\xb7\xa8\xe7\x89\xb9\xe5\x88\xa5\xe5\x90\x8d\xe5\x96\xae \xe3\x80\x91\x1b[22;23H(\x1b[1;36mG\x1b[m)oodbye\x1b[22;41H\xe9\x9b\xa2\xe9\x96\x8b\xef\xbc\x8c\xe5\x86\x8d\xe8\xa6\x8b \x08\x08\xe2\x80\xa6\x1b[22;53H\r\n\n\x1b[34;46m[12/4 \xe6\x98\x9f\xe6\x9c\x9f\xe5\x85\xad 14:26]\x1b[1;33;45m [ \xe9\x9b\x99\xe9\xad\x9a\xe6\x99\x82 ] \x1b[30;47m \xe7\xb7\x9a\xe4\xb8\x8a\x1b[31m66391\x1b[30m\xe4\xba\xba, \xe6\x88\x91\xe6\x98\xaf\x1b[31mCodingMan\x1b[30m [\xe5\x91\xbc\xe5\x8f\xab\xe5\x99\xa8]\x1b[31m\xe9\x97\x9c\xe9\x96\x89 \x1b[m\x1b[15;21H\x1b[H\x1b[2J\x1b[1;37;44m\xe3\x80\x90\xe6\x9d\xbf\xe4\xb8\xbb:catcatcatcat\xe3\x80\x91 \x1b[33mPython \xe7\xa8\x8b\xe5\xbc\x8f\xe8\xaa\x9e\xe8\xa8\x80\x1b[0;1;37;44m \xe7\x9c\x8b\xe6\x9d\xbf\xe3\x80\x8aPython\xe3\x80\x8b\r\n\x1b[m[ \x08\x08\xe2\x86\x90\x1b[2;4H]\xe9\x9b\xa2\xe9\x96\x8b [ \x08\x08\xe2\x86\x92\x1b[2;13H]\xe9\x96\xb1\xe8\xae\x80 [Ctrl-P]\xe7\x99\xbc\xe8\xa1\xa8\xe6\x96\x87\xe7\xab\xa0 [d]\xe5\x88\xaa\xe9\x99\xa4 [z]\xe7\xb2\xbe\xe8\x8f\xaf\xe5\x8d\x80 [i]\xe7\x9c\x8b\xe6\x9d\xbf\xe8\xb3\x87\xe8\xa8\x8a/\xe8\xa8\xad\xe5\xae\x9a [h]\xe8\xaa\xaa\xe6\x98\x8e\r\n\x1b[30;47m \xe7\xb7\xa8\xe8\x99\x9f \xe6\x97\xa5 \xe6\x9c\x9f \xe4\xbd\x9c \xe8\x80\x85 \xe6\x96\x87 \xe7\xab\xa0 \xe6\xa8\x99 \xe9\xa1\x8c \xe4\xba\xba\xe6\xb0\xa3:5 \x1b[4;4H\x1b[m8861 +\x1b[1;32m 2\x1b[m11/18 zj4gjcl6 \x08\x08\xe2\x96\xa1\x1b[4;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe7\xb0\xa1\xe5\x96\xae\xe7\x9a\x84\xe6\xad\xa3\xe5\x89\x87\xe8\xa1\xa8\xe9\x81\x94\xe5\xbc\x8f\xe8\xa8\x98\xe6\xb3\x95?\x1b[5;4H8862 +\x1b[1;32m 1\x1b[m11/18 d8888\x1b[5;31HR: [\xe9\x96\x92\xe8\x81\x8a] \xe6\x95\xb8\xe5\xad\xb8\xe4\xb8\x8d\xe5\xa5\xbd\xe6\x80\x8e\xe9\xba\xbc\xe7\x8e\xa9AI??\x1b[6;4H8863 +\x1b[1;32m 7\x1b[m11/18 \x1b[1;37mVivianAnn \x08\x08\x1b[m\xe2\x96\xa1\x1b[6;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe7\x94\xa8pip\xe7\x84\xa1\xe6\xb3\x95\xe5\xae\x89\xe8\xa3\x9dlibyang\x1b[7;4H8864 +\x1b[1;30m \x1b[m11/18 garlic774 \x08\x08\xe2\x96\xa1\x1b[7;33H [\xe9\x96\x92\xe8\x81\x8a] \xe8\xab\x8b\xe6\x95\x99\xe5\xa6\x82\xe4\xbd\x95\xe6\x8a\x93class\xe4\xb8\x8b\xe9\x9d\xa2\xe7\x9a\x84\xe8\xb3\x87\xe8\xa8\x8a\x1b[8;4H8865 +\x1b[1;30m \x1b[m11/20 garlic774 \x08\x08\xe2\x96\xa1\x1b[8;33H [\xe5\x95\x8f\xe9\xa1\x8c] Xpath\xe6\x8a\x93\xe4\xb8\x8d\xe5\x88\xb0\xe5\x85\xa7\xe5\xae\xb9\x1b[9;4H8866 +\x1b[1;32m 1\x1b[m11/22 g919233 \x08\x08\xe2\x96\xa1\x1b[9;33H [\xe6\x95\x99\xe5\xad\xb8] \xe7\x80\x8f\xe8\xa6\xbd\xe5\x99\xa8\xe8\x87\xaa\xe5\x8b\x95\xe5\x8c\x96\xe5\xb7\xa5\xe5\x85\xb7 Playwright\xef\xbc\x8c\xe7\x94\xa8\xe6\x96\xbc\xe7\xb6\xb2\x1b[10;4H8867 ~\x1b[1;32m 3\x1b[m11/23 ajjj840569 \x08\x08\xe2\x96\xa1\x1b[10;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe5\xa6\x82\xe6\x9e\x9c\xe8\xae\x93\xe7\x84\xa1\xe9\x96\x93\xe9\x9a\x94\xe7\x9a\x84\xe9\x80\xa3\xe7\xba\x8c\xe5\xad\x97 \xe6\x8e\xa8\xe5\xbe\x97\xe5\xad\x97\xe5\x85\xb8\xe5\xb0\x8d\xe6\x87\x89\xe5\x80\xbc\xef\xbc\x9f\x1b[11;4H8868 +\x1b[1;32m 2\x1b[m11/23 giuk0717 \x08\x08\xe2\x96\xa1\x1b[11;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe6\x89\xbe\xe5\x87\xba\xe6\x9c\x80\xe9\x95\xb7\xe9\x9b\x86\xe5\x90\x88\x1b[12;4H8869 +\x1b[1;30m \x1b[m11/23 areyo\x1b[12;31H \x08\x08\xe2\x96\xa1\x1b[12;33H [\xe5\x95\x8f\xe9\xa1\x8c] django + apache print\xe4\xb8\xad\xe6\x96\x87\x1b[13;4H8870 +\x1b[1;32m 3\x1b[m11/24 dreambegins \x08\x08\xe2\x96\xa1\x1b[13;33H [\xe5\x95\x8f\xe9\xa1\x8c] Robot Framework\xe7\x9a\x84\xe8\xaa\x9e\xe6\xb3\x95\xe5\x95\x8f\xe9\xa1\x8c\x1b[14;4H8871 +\x1b[1;30m \x1b[m11/27 DiamondAse \x08\x08\xe2\x96\xa1\x1b[14;33H [\xe5\x95\x8f\xe9\xa1\x8c] selenium\xe6\x8a\x93chrom\xe9\x96\x8b\xe8\xb5\xb7\xe7\x9a\x84pdf\xe7\xb6\xb2\xe9\xa0\x81\xe5\x85\x83\xe7\xb4\xa0\xe6\x8a\x93\xe4\xb8\x8d\xe5\x88\xb0\x1b[15;4H8872 +\x1b[1;32m 1\x1b[m11/28 garlic774 \x08\x08\xe2\x96\xa1\x1b[15;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe5\xa6\x82\xe4\xbd\x95\xe5\x9c\xa8chrome\xe6\x93\xb4\xe5\x85\x85\xe5\xbe\x97\xe5\x88\xb0\xe6\xaa\xa2\xe8\xa6\x96\xe5\x85\x83\xe7\xb4\xa0\xef\xbc\x9f\x1b[16;4H8873 +\x1b[1;30m \x1b[m11/28 ruthertw \x08\x08\xe2\x96\xa1\x1b[16;33H [\xe5\x95\x8f\xe9\xa1\x8c] np.transpose\xe7\x9a\x84\xe7\x94\xa8\xe6\xb3\x95\x1b[17;4H8874 +\x1b[1;30m \x1b[m11/28 nicha115 \x08\x08\xe2\x96\xa1\x1b[17;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe8\xab\x8b\xe5\x95\x8ftrace\xe5\x8e\x9f\xe5\xa7\x8b\xe7\xa2\xbc\xe5\x95\x8f\xe9\xa1\x8c\x1b[18;4H8875 +\x1b[1;33m10\x1b[m11/29 \x1b[1;37mVivianAnn \x08\x08\x1b[m\xe2\x96\xa1\x1b[18;33H [\xe5\x95\x8f\xe9\xa1\x8c] leetcode 2029 (Hard) \xe7\x9a\x84\xe5\x95\x8f\xe9\xa1\x8c\x1b[19;4H8876 +\x1b[1;32m 3\x1b[m11/29 a199111222 \x08\x08\xe2\x96\xa1\x1b[19;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe7\x88\xac\xe8\x9f\xb2\xe7\xaa\x81\xe7\x84\xb6\xe4\xb8\x8d\xe8\x83\xbd\xe8\xb7\x91\xef\xbc\x8c\xe6\xb1\x82\xe8\xa7\xa3\x1b[20;4H8877 +\x1b[1;32m 1\x1b[m11/30 Rasin\x1b[20;31H \x08\x08\xe2\x96\xa1\x1b[20;33H [\xe5\x95\x8f\xe9\xa1\x8c] numpy dimension\x1b[21;4H8878 +\x1b[1;32m 3\x1b[m11/30 Moonmoon0827 \x08\x08\xe2\x96\xa1\x1b[21;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe6\x96\xb0\xe6\x89\x8b list to string \xe7\x9a\x84\xe5\x95\x8f\xe9\xa1\x8c\x1b[22;4H8879 +\x1b[1;32m 3\x1b[m12/01 m0911182606 \x08\x08\xe2\x96\xa1\x1b[22;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe8\xae\x80\xe5\x8f\x96/\xe4\xbf\xae\xe6\x94\xb9\xe6\xaa\x94\xe6\xa1\x88\xe5\x85\xa7\xe5\xae\xb9\xe6\x8c\x87\xe5\xae\x9a\xe5\x8d\x80\xe9\x96\x93\xe6\x96\x87\xe5\xad\x97\r\n> 8880 ~\x1b[1;32m 4\x1b[m12/02 stepfish \x08\x08\xe2\x96\xa1\x1b[23;33H [\xe8\xb3\x87\xe8\xa8\x8a] \xe5\xb0\x88\xe5\xb1\xac\xe5\xa5\xb3\xe7\x94\x9f\xe7\x9a\x84Python\xe5\x85\xa5\xe9\x96\x80\xe8\xaa\xb2\xef\xbc\x88Pyladies\xe4\xb8\xbb\xe8\xbe\xa6\xef\xbc\x89\r\n\x1b[34;46m \xe6\x96\x87\xe7\xab\xa0\xe9\x81\xb8\xe8\xae\x80 \x1b[30;47m \x1b[31m(y)\x1b[30m\xe5\x9b\x9e\xe6\x87\x89\x1b[31m(X)\x1b[30m\xe6\x8e\xa8\xe6\x96\x87\x1b[31m(^X)\x1b[30m\xe8\xbd\x89\xe9\x8c\x84 \x1b[31m(=[]<>)\x1b[30m\xe7\x9b\xb8\xe9\x97\x9c\xe4\xb8\xbb\xe9\xa1\x8c\x1b[31m(/?a)\x1b[30m\xe6\x89\xbe\xe6\xa8\x99\xe9\xa1\x8c/\xe4\xbd\x9c\xe8\x80\x85 \x1b[31m(b)\x1b[30m\xe9\x80\xb2\xe6\x9d\xbf\xe7\x95\xab\xe9\x9d\xa2 \x1b[m\x1b[23;1H" # main menu screen = b'7;61H \x08\x08\x1b[m\xe2\x97\xa4\x1b[7;65H \x08\x08\xe2\x80\x94\x1b[7;73H\xef\xbc\x8b\x1b[8;17H1 \xe5\xb8\x82\xe6\xb0\x91\xe5\xbb\xa3\xe5\xa0\xb4 \xe5\xa0\xb1\xe5\x91\x8a\xe7\xab\x99\xe9\x95\xb7 PTT\xe5\x92\xac\xe6\x88\x91\x1b[9;17H2 \xe8\x87\xba\xe7\x81\xa3\xe5\xa4\xa7\xe5\xad\xb8 \xe8\x87\xba\xe5\xa4\xa7, \xe8\x87\xba\xe5\xa4\xa7, \xe8\x87\xba\xe5\xa4\xa7\x1b[9;62H[paullai/s75\x1b[10;17H3 \xe6\x94\xbf\xe6\xb2\xbb\xe5\xa4\xa7\xe5\xad\xb8 \xe6\x94\xbf\xe5\xa4\xa7, \xe6\x94\xbf\xe5\xa4\xa7, \xe6\x94\xbf\xe5\xa4\xa7\x1b[10;62H[steve1121/s\x1b[11;17H4 \xe9\x9d\x92\xe8\x98\x8b\xe6\x9e\x9c\xe6\xa8\xb9 \xe6\xa0\xa1\xe5\x9c\x92, \xe7\x8f\xad\xe6\x9d\xbf, \xe7\xa4\xbe\xe5\x9c\x98\x1b[11;62H[dreamwave/f\x1b[12;17H5 \xe6\xb4\xbb\xe5\x8b\x95\xe4\xb8\xad\xe5\xbf\x83 \xe7\xa4\xbe\xe5\x9c\x98, \xe8\x81\x9a\xe6\x9c\x83, \xe5\x9c\x98\xe9\xab\x94\x1b[12;62H[dreamwave/s\x1b[13;17H6 \xe8\xa6\x96\xe8\x81\xbd\xe5\x8a\x87\xe5\xa0\xb4 \xe5\x81\xb6\xe5\x83\x8f, \xe9\x9f\xb3\xe6\xa8\x82, \xe5\xbb\xa3\xe9\x9b\xbb\x1b[13;62H[mousepad\xe4\xbb\xa3]\x1b[14;17H7 \xe6\x88\xb0\xe7\x95\xa5\xe9\xab\x98\xe6\x89\x8b \xe9\x81\x8a\xe6\x88\xb2, \xe6\x95\xb8\xe4\xbd\x8d, \xe7\xa8\x8b\xe8\xa8\xad\x1b[14;62H[a3225737]\x1b[15;17H8 \xe5\x8d\xa1\xe6\xbc\xab\xe5\xa4\xa2\xe5\xb7\xa5\xe5\xbb\xa0 \xe5\x8d\xa1\xe9\x80\x9a, \xe6\xbc\xab\xe7\x95\xab, \xe5\x8b\x95\xe7\x95\xab\x1b[15;62H[hay955940/k\x1b[16;17H9 \xe7\x94\x9f\xe6\xb4\xbb\xe5\xa8\x9b\xe6\xa8\x82\xe9\xa4\xa8 \xe7\x94\x9f\xe6\xb4\xbb, \xe5\xa8\x9b\xe6\xa8\x82, \xe5\xbf\x83\xe6\x83\x85\x1b[16;62H[Bignana/sky\x1b[17;16H10 \xe5\x9c\x8b\xe5\xae\xb6\xe7\xa0\x94\xe7\xa9\xb6\xe9\x99\xa2 \xe6\x94\xbf\xe6\xb2\xbb, \xe6\x96\x87\xe5\xad\xb8, \xe5\xad\xb8\xe8\xa1\x93\x1b[17;62H[JosephChen/\x1b[18;16H11 \xe5\x9c\x8b\xe5\xae\xb6\xe9\xab\x94\xe8\x82\xb2\xe5\xa0\xb4 \xe6\xb1\x97\xe6\xb0\xb4, \xe9\xac\xa5\xe5\xbf\x97, \xe8\x86\xbd\xe8\xad\x98\x1b[18;62H[JUNstudio]\x1b[19;16H12 \x08\x08\x1b[1;31m\xcb\x87\x1b[19;21H\x1b[m[\xe4\xb8\x89\xe9\x87\x91] \xe9\x87\x91\xe9\xa6\xac58 \xe7\x80\x91\xe5\xb8\x83 \xe5\x9b\x9b\xe7\x8d\x8e\xe5\xa4\xa7\xe8\xb4\x8f\xe5\xae\xb6\x1b[19;62HFelix76116/s\x1b[20;16H13 \x08\x08\x1b[1;31m\xcb\x87\x1b[20;21H\x1b[m[\xe6\xad\xa6\xe6\xbc\xa2\xe8\x82\xba\xe7\x82\x8e] \xe5\x85\xa8\xe5\x9c\x8b\xe4\xba\x8c\xe7\xb4\x9a\xe8\xad\xa6\xe6\x88\x92\x1b[20;62Hswattw/flyin\x1b[21;16H14 \x08\x08\x1b[1;31m\xcb\x87\x1b[21;21H\x1b[m\xe3\x80\x8aQuestCenter\xe3\x80\x8b\xe7\x9c\x8b\xe6\x9d\xbf\xe8\xb3\x87\xe8\xa8\x8a\xe5\x85\xac\xe5\xb8\x83\xe6\xac\x84\x1b[21;62Hzhibb/inhuma\x1b[22;16H15 --> \xe5\x8d\xb3\xe6\x99\x82\xe7\x86\xb1\xe9\x96\x80\xe7\x9c\x8b\xe6\x9d\xbf <--\x1b[23;11H> 16 \x08\x08\x1b[1;31m\xcb\x87\x1b[23;21H\x1b[m[\xe6\xb4\xbb\xe5\x8b\x95] \xe6\x9c\x8d\xe5\x8b\x99\xe8\xad\x89\xe6\x9b\xb8\xe7\x94\xb3\xe8\xab\x8b 12/1-12/10\x1b[23;62HVal/chuo/han\x1b[23;11H\x1b[H\x1b[2J\x1b[1;37;44m\xe3\x80\x90\xe4\xb8\xbb\xe5\x8a\x9f\xe8\x83\xbd\xe8\xa1\xa8\xe3\x80\x91 \x1b[33m\xe6\x89\xb9\xe8\xb8\xa2\xe8\xb8\xa2\xe5\xaf\xa6\xe6\xa5\xad\xe5\x9d\x8a\x1b[0;1;37;44m \x1b[2;7H \x08\x08\x1b[0;1;37m\xe2\x97\x8f\x1b[2;9H\x1b[41m \x08\x08\xe2\x97\xa2\x1b[2;15H \x08\x08\x1b[47m\xe2\x97\xa4\x1b[2;17H \x08\x08\x1b[0;30;47m\xe2\x96\x85\x1b[2;19H \x08\x08\xe2\x96\x85\x1b[2;21H \x08\x08\x1b[m\xe2\x97\xa3\x1b[2;23H \x08\x08\x1b[1;37m\xe2\x97\xa2\x1b[2;31H \x08\x08\xe2\x96\x88\x1b[2;33H \x08\x08\xe2\x96\x88\x1b[2;35H \x08\x08\xe2\x96\x88\x1b[2;37H \x08\x08\xe2\x96\x88\x1b[2;39H \x08\x08\xe2\x96\x88\x1b[2;41H \x08\x08\xe2\x96\x88\x1b[2;43H \x08\x08\xe2\x96\x88\x1b[2;45H \x08\x08\xe2\x97\xa3\x1b[2;47H \x08\x08\xe2\x97\xa2\x1b[2;53H \x08\x08\xe2\x97\xa3\x1b[2;55H \x08\x08\xe2\x97\xa2\x1b[2;61H \x08\x08\xe2\x97\xa3\x1b[2;63H \x08\x08\xe2\x97\xa2\x1b[2;69H \x08\x08\xe2\x96\x88\x1b[2;71H \x08\x08\xe2\x96\x88\x1b[2;73H \x08\x08\xe2\x96\x88\x1b[2;75H \x08\x08\xe2\x96\x88\x1b[2;77H \x08\x08\xe2\x96\x88\x1b[2;79H\x1b[3;3H \x08\x08\xe2\x97\xa2\x1b[3;5H \x08\x08\xe2\x97\xa3\x1b[3;7H \x08\x08\x1b[0;31m\xe2\x97\xa5\x1b[3;11H \x08\x08\x1b[0;1;37;41m\xe2\x97\xa2\x1b[3;13H \x08\x08\x1b[47m\xe2\x97\xa4\x1b[3;15H \x08\x08\x1b[0;30;47m\xe2\x97\x8f\x1b[3;20H \x08\x08\x1b[0;1;31m\xe2\x97\xa3\x1b[3;25H \x08\x08\x1b[0;1;37m\xe2\x97\xa2\x1b[3;29H \x08\x08\x1b[46m\xe2\x97\xa4\x1b[3;31H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;33H \x08\x08\x1b[46m\xe2\x97\xa4\x1b[3;35H \x08\x08\xe2\x97\xa5\x1b[3;37H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;39H \x08\x08\x1b[46m\xe2\x97\xa5\x1b[3;41H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;43H \x08\x08\x1b[46m\xe2\x97\xa5\x1b[3;45H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;47H \x08\x08\xe2\x97\xa3\x1b[3;49H \x08\x08\xe2\x97\xa2\x1b[3;51H \x08\x08\xe2\x96\x88\x1b[3;53H \x08\x08\xe2\x96\x88\x1b[3;55H \x08\x08\xe2\x97\xa3\x1b[3;57H \x08\x08\xe2\x97\xa2\x1b[3;59H \x08\x08\xe2\x96\x88\x1b[3;61H \x08\x08\xe2\x96\x88\x1b[3;63H \x08\x08\xe2\x97\xa3\x1b[3;65H \x08\x08\xe2\x97\xa2\x1b[3;67H \x08\x08\x1b[46m\xe2\x97\xa4\x1b[3;69H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;71H \x08\x08\x1b[46m\xe2\x97\xa4\x1b[3;73H \x08\x08\xe2\x97\xa5\x1b[3;75H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;77H \x08\x08\x1b[46m\xe2\x97\xa5\x1b[3;79H\r\n \x08\x08\x1b[0;1;37m\xe2\x97\xa2\x1b[4;3H \x08\x08\xe2\x96\x88\x1b[4;5H \x08\x08\xe2\x96\x88\x1b[4;7H \x08\x08\xe2\x97\xa3\x1b[4;9H \x08\x08\x1b[47m\xe2\x97\xa4\x1b[4;13H \x1b[0;36m \x08\x08\xe2\x97\xa2\x1b[4;27H \x08\x08\x1b[0;1;37;46m\xe2\x97\xa4\x1b[4;29H \x08\x08\xe2\x97\xa4\x1b[4;33H \x08\x08\xe2\x97\xa5\x1b[4;39H \x08\x08\xe2\x97\xa5\x1b[4;43H \x08\x08\xe2\x97\xa5\x1b[4;47H \x08\x08\xe2\x97\xa5\x1b[4;49H \x08\x08\xe2\x97\xa4\x1b[4;51H \x08\x08\xe2\x97\xa4\x1b[4;53H \x08\x08\xe2\x97\xa4\x1b[4;55H \x08\x08\xe2\x97\xa5\x1b[4;57H \x08\x08\xe2\x97\xa4\x1b[4;59H \x08\x08\xe2\x97\xa4\x1b[4;61H \x08\x08\xe2\x97\xa4\x1b[4;63H \x08\x08\xe2\x97\xa5\x1b[4;65H \x08\x08\xe2\x97\xa4\x1b[4;67H \x08\x08\xe2\x97\xa4\x1b[4;71H \x08\x08\xe2\x97\xa5\x1b[4;77H \x08\x08\x1b[0;36m\xe2\x96\x88\x1b[4;79H\r\n \x08\x08\x1b[0;1;37;46m\xe2\x97\xa4\x1b[5;3H \x08\x08\xe2\x97\xa4\x1b[5;5H \x08\x08\xe2\x97\xa4\x1b[5;7H \x08\x08\xe2\x97\xa5\x1b[5;9H \x08\x08\x1b[0;36m\xe2\x97\xa3\x1b[5;11H \x08\x08\x1b[m\xe2\x97\xa5\x1b[5;13H\x1b[31;47m \x08\x08\xe2\x97\xa5\x1b[5;20H\x1b[0;41m_ \x08\x08\x1b[0;31m\xe2\x97\xa4\x1b[5;23H \x08\x08\x1b[36m\xe2\x97\xa2\x1b[5;25H\x1b[0;1;37;46m \x08\x08\xe2\x95\xad\x1b[5;31H \x08\x08\xe2\x95\xae\x1b[5;33H \x08\x08\xe2\x94\x82\x1b[5;35H \x08\x08\xe2\x95\xad\x1b[5;41H \x08\x08\xe2\x95\xae\x1b[5;43H \x08\x08\xe2\x94\x82\x1b[5;59H \x08\x08\xe2\x94\x82\x1b[5;77H \r\n\x1b[0;46m \x08\x08\x1b[0;36m\xe2\x97\xa4\x1b[6;11H \x08\x08\x1b[31m\xe2\x97\xa2\x1b[6;13H\x1b[41m \x1b[0;31m \x1b[0;1;37;46m \x08\x08\xe2\x94\x82\x1b[6;31H \x08\x08\xe2\x94\x82\x1b[6;33H \x08\x08\xe2\x94\x82\x1b[6;35H \x08\x08\xe2\x88\x95\x1b[6;37H\xe3\x80\x82 \x08\x08\xe2\x94\x82\x1b[6;41H \x08\x08\xe2\x94\x82\x1b[6;43H \x08\x08\xe2\x95\xad\x1b[6;45H \x08\x08\xe2\x95\xae\x1b[6;47H \x08\x08\xe2\x95\xad\x1b[6;49H \x08\x08\xe2\x95\xae\x1b[6;51H \x08\x08\xe2\x94\x82\x1b[6;53H \x08\x08\xe2\x94\x82\x1b[6;57H \x08\x08\xe2\x94\x9c\x1b[6;59H \x08\x08\xe2\x95\xae\x1b[6;61H \x08\x08\xe2\x95\xad\x1b[6;63H \x08\x08\xe2\x95\xae\x1b[6;65H \x08\x08\xe2\x95\xad\x1b[6;67H \x08\x08\xe2\x95\xae\x1b[6;69H \x08\x08\xe2\x95\xad\x1b[6;71H \x08\x08\xe2\x95\xad\x1b[6;73H \x08\x08\xe2\x95\xad\x1b[6;75H \x08\x08\xe2\x94\xa4\x1b[6;77H \x1b[7;9H \x08\x08\x1b[m\xe2\x97\xa2\x1b[7;11H \x08\x08\x1b[31;47m\xe2\x97\xa2\x1b[7;13H \x08\x08\xe2\x97\xa4\x1b[7;15H \x08\x08\x1b[30m\xe2\x95\xb2\x1b[7;17H_ \x08\x08\x1b[m\xe2\x97\xa3\x1b[7;23H \x08\x08\x1b[1;37m\xe2\x95\xb0\x1b[7;31H \x08\x08\xe2\x95\xae\x1b[7;33H \x08\x08\xe2\x94\x9c\x1b[7;35H \x08\x08\xe2\x95\xae\x1b[7;37H \x08\x08\xe2\x94\x82\x1b[7;39H \x08\x08\xe2\x95\xb0\x1b[7;41H \x08\x08\xe2\x95\xae\x1b[7;43H \x08\x08\xe2\x94\x82\x1b[7;45H \x08\x08\xe2\x94\x82\x1b[7;47H \x08\x08\xe2\x94\x82\x1b[7;49H \x08\x08\xe2\x94\x82\x1b[7;51H \x08\x08\xe2\x94\x82\x1b[7;53H \x08\x08\xe2\x94\x82\x1b[7;55H \x08\x08\xe2\x94\x82\x1b[7;57H \x08\x08\xe2\x94\x82\x1b[7;59H \x08\x08\xe2\x94\x82\x1b[7;61H \x08\x08\xe2\x94\x82\x1b[7;63H \x08\x08\xe2\x94\x82\x1b[7;65H \x08\x08\xe2\x95\xad\x1b[7;67H \x08\x08\xe2\x94\xa4\x1b[7;69H \x08\x08\xe2\x94\x9c\x1b[7;71H \x08\x08\xe2\x95\xaf\x1b[7;73H \x08\x08\xe2\x94\x82\x1b[7;75H \x08\x08\xe2\x94\x82\x1b[7;77H\r\n \x08\x08\x1b[30m\xe2\x95\xad\x1b[8;3H \x08\x08\xe2\x94\x80\x1b[8;5H \x08\x08\xe2\x94\x80\x1b[8;7H \x08\x08\xe2\x94\x80\x1b[8;9H \x08\x08\x1b[0;31;47m\xe2\x97\xa2\x1b[8;11H \x08\x08\xe2\x97\xa4\x1b[8;13H \x08\x08\x1b[30m\xe2\x97\xa5\x1b[8;20H \x08\x08\xe2\x96\x8e\x1b[8;22H \x1b[0;30m \x1b[0;1;37;44m \x08\x08\xe2\x94\x82\x1b[8;31H \x08\x08\xe2\x94\x82\x1b[8;33H \x08\x08\xe2\x94\x82\x1b[8;35H \x08\x08\xe2\x94\x82\x1b[8;37H \x08\x08\xe2\x94\x82\x1b[8;41H \x08\x08\xe2\x94\x82\x1b[8;43H \x08\x08\xe2\x94\x82\x1b[8;45H \x08\x08\xe2\x94\x82\x1b[8;47H \x08\x08\xe2\x95\xb0\x1b[8;49H \x08\x08\xe2\x95\xaf\x1b[8;51H \x08\x08\xe2\x95\xb0\x1b[8;53H \x08\x08\xe2\x94\xb4\x1b[8;55H \x08\x08\xe2\x95\xaf\x1b[8;57H \x08\x08\xe2\x94\x9c\x1b[8;59H \x08\x08\xe2\x95\xaf\x1b[8;61H \x08\x08\xe2\x95\xb0\x1b[8;63H \x08\x08\xe2\x95\xaf\x1b[8;65H \x08\x08\xe2\x95\xb0\x1b[8;67H \x08\x08\xe2\x94\xa4\x1b[8;69H \x08\x08\xe2\x94\x82\x1b[8;71H \x08\x08\xe2\x95\xb0\x1b[8;75H \x08\x08\xe2\x94\xa4\x1b[8;77H \r\n \x08\x08\x1b[0;1;30m\xe2\x95\xb0\x1b[9;3H \x08\x08\xe2\x94\x80\x1b[9;5H \x08\x08\xe2\x94\x80\x1b[9;7H \x08\x08\xe2\x94\x80\x1b[9;9H\x1b[0;30;47m ____ \x08\x08\xe2\x95\xb1\x1b[9;20H \x1b[0;30m \x1b[0;1;37;44m \x08\x08\xe2\x95\xb0\x1b[9;31H \x08\x08\xe2\x95\xaf\x1b[9;33H \x08\x08\xe2\x94\x82\x1b[9;37H \x08\x08\xe2\x95\xb0\x1b[9;41H \x08\x08\xe2\x95\xaf\x1b[9;43H \r\n \x08\x08\x1b[0;34m\xe2\x96\x84\x1b[10;3H \x08\x08\xe2\x96\x84\x1b[10;5H \x08\x08\xe2\x96\x84\x1b[10;7H \x1b[30;47m \x08\x08\xe2\x97\xa2\x1b[10;15H \x08\x08\xe2\x97\xa4\x1b[10;17H \x1b[0;30m \x1b[0;1;37;44m \x08\x08\xe2\x94\x82\x1b[10;37H \x1b[36m\xe6\xad\xa1 \xe8\xbf\x8e \xe5\xa4\xa7 \xe5\xae\xb6 \xe4\xbe\x86 \xe6\xbb\x91 \xe9\x9b\xaa \xe6\x9d\xbf \xe9\x80\x9b \xe9\x80\x9b \xe5\x93\xa6 ! \x1b[11;9H \x08\x08\x1b[m\xe2\x97\xa5\x1b[11;11H \x08\x08\x1b[30;47m\xe2\x97\xa2\x1b[11;13H \x08\x08\xe2\x97\xa4\x1b[11;15H \x08\x08\x1b[m\xe2\x97\xa4\x1b[11;23H \x08\x08\x1b[33m\xe2\x97\xa2\x1b[11;25H\x1b[43m \x1b[0;33m \x1b[44m \x1b[12;2H\x1b[1;30;43mby fuxk \x08\x08\x1b[0;30;43m\xe2\x97\xa2\x1b[12;11H \x08\x08\xe2\x97\xa4\x1b[12;13H\x1b[1mfuxk fuxk \x08\x08\x1b[0;33m\xe2\x97\xa4\x1b[12;25H \x1b[1;44m\xe3\x80\x90\xe5\x9c\x8b\xe5\xae\xb6\xe9\xab\x94\xe8\x82\xb2\xe5\xa0\xb4\xe3\x80\x91 \x08\x08\xe2\x86\x92\x1b[12;46H \xe3\x80\x90PttSport\xe3\x80\x91 \x08\x08\xe2\x86\x92\x1b[12;62H \xe3\x80\x90SkiSnowboard\xe3\x80\x91\x1b[13;23H\x1b[m(\x1b[1;36mA\x1b[m)nnounce \xe3\x80\x90 \xe7\xb2\xbe\xe8\x8f\xaf\xe5\x85\xac\xe4\xbd\x88\xe6\xac\x84 \xe3\x80\x91\x1b[14;23H(\x1b[1;36mF\x1b[m)avorite \xe3\x80\x90 \xe6\x88\x91 \xe7\x9a\x84 \xe6\x9c\x80\xe6\x84\x9b \xe3\x80\x91\x1b[15;23H(\x1b[1;36mC\x1b[m)lass\x1b[15;38H\xe3\x80\x90 \xe5\x88\x86\xe7\xb5\x84\xe8\xa8\x8e\xe8\xab\x96\xe5\x8d\x80 \xe3\x80\x91\x1b[16;23H(\x1b[1;36mM\x1b[m)ail\x1b[16;38H\xe3\x80\x90 \xe7\xa7\x81\xe4\xba\xba\xe4\xbf\xa1\xe4\xbb\xb6\xe5\x8d\x80 \xe3\x80\x91\x1b[17;23H(\x1b[1;36mT\x1b[m)alk\x1b[17;38H\xe3\x80\x90 \xe4\xbc\x91\xe9\x96\x92\xe8\x81\x8a\xe5\xa4\xa9\xe5\x8d\x80 \xe3\x80\x91\x1b[18;23H(\x1b[1;36mU\x1b[m)ser\x1b[18;38H\xe3\x80\x90 \xe5\x80\x8b\xe4\xba\xba\xe8\xa8\xad\xe5\xae\x9a\xe5\x8d\x80 \xe3\x80\x91\x1b[19;23H(\x1b[1;36mX\x1b[m)yz\x1b[19;38H\xe3\x80\x90 \xe7\xb3\xbb\xe7\xb5\xb1\xe8\xb3\x87\xe8\xa8\x8a\xe5\x8d\x80 \xe3\x80\x91\x1b[20;23H(\x1b[1;36mP\x1b[m)lay\x1b[20;38H\xe3\x80\x90 \xe5\xa8\x9b\xe6\xa8\x82\xe8\x88\x87\xe4\xbc\x91\xe9\x96\x92 \xe3\x80\x91\x1b[21;23H(\x1b[1;36mN\x1b[m)amelist \xe3\x80\x90 \xe7\xb7\xa8\xe7\x89\xb9\xe5\x88\xa5\xe5\x90\x8d\xe5\x96\xae \xe3\x80\x91\x1b[22;21H> (\x1b[1;36mG\x1b[m)oodbye\x1b[22;41H\xe9\x9b\xa2\xe9\x96\x8b\xef\xbc\x8c\xe5\x86\x8d\xe8\xa6\x8b \x08\x08\xe2\x80\xa6\x1b[22;53H\r\n\n\x1b[34;46m[12/4 \xe6\x98\x9f\xe6\x9c\x9f\xe5\x85\xad 16:24]\x1b[1;33;45m [ \xe5\xb0\x84\xe6\x89\x8b\xe6\x99\x82 ] \x1b[30;47m \xe7\xb7\x9a\xe4\xb8\x8a\x1b[31m66945\x1b[30m\xe4\xba\xba, \xe6\x88\x91\xe6\x98\xaf\x1b[31mCodingMan\x1b[30m [\xe5\x91\xbc\xe5\x8f\xab\xe5\x99\xa8]\x1b[31m\xe9\x97\x9c\xe9\x96\x89 \x1b[m\x1b[22;21H' # query post screen = b'\x1b[H\x1b[2J\x1b[1;30m\xe3\x80\x90\xe6\x9d\xbf\xe4\xb8\xbb:catcatcatcat\xe3\x80\x91 Python \xe7\xa8\x8b\xe5\xbc\x8f\xe8\xaa\x9e\xe8\xa8\x80 \xe7\x9c\x8b\xe6\x9d\xbf\xe3\x80\x8aPython\xe3\x80\x8b \r\n[ \x08\x08\xe2\x86\x90\x1b[2;4H]\xe9\x9b\xa2\xe9\x96\x8b [ \x08\x08\xe2\x86\x92\x1b[2;13H]\xe9\x96\xb1\xe8\xae\x80 [Ctrl-P]\xe7\x99\xbc\xe8\xa1\xa8\xe6\x96\x87\xe7\xab\xa0 [d]\xe5\x88\xaa\xe9\x99\xa4 [z]\xe7\xb2\xbe\xe8\x8f\xaf\xe5\x8d\x80 [i]\xe7\x9c\x8b\xe6\x9d\xbf\xe8\xb3\x87\xe8\xa8\x8a/\xe8\xa8\xad\xe5\xae\x9a [h]\xe8\xaa\xaa\xe6\x98\x8e \r\n\n\x1b[0;1;37m> 1 112/09 ericsk \x08\x08\xe2\x96\xa1\x1b[4;33H [\xe5\xbf\x83\xe5\xbe\x97] \xe7\xb5\x82\xe6\x96\xbc\xe9\x96\x8b\xe6\x9d\xbf\xe4\xba\x86 \r\n \x08\x08\x1b[m\xe2\x94\x8c\x1b[5;3H \x08\x08\xe2\x94\x80\x1b[5;5H \x08\x08\xe2\x94\x80\x1b[5;7H \x08\x08\xe2\x94\x80\x1b[5;9H \x08\x08\xe2\x94\x80\x1b[5;11H \x08\x08\xe2\x94\x80\x1b[5;13H \x08\x08\xe2\x94\x80\x1b[5;15H \x08\x08\xe2\x94\x80\x1b[5;17H \x08\x08\xe2\x94\x80\x1b[5;19H \x08\x08\xe2\x94\x80\x1b[5;21H \x08\x08\xe2\x94\x80\x1b[5;23H \x08\x08\xe2\x94\x80\x1b[5;25H \x08\x08\xe2\x94\x80\x1b[5;27H \x08\x08\xe2\x94\x80\x1b[5;29H \x08\x08\xe2\x94\x80\x1b[5;31H \x08\x08\xe2\x94\x80\x1b[5;33H \x08\x08\xe2\x94\x80\x1b[5;35H \x08\x08\xe2\x94\x80\x1b[5;37H \x08\x08\xe2\x94\x80\x1b[5;39H \x08\x08\xe2\x94\x80\x1b[5;41H \x08\x08\xe2\x94\x80\x1b[5;43H \x08\x08\xe2\x94\x80\x1b[5;45H \x08\x08\xe2\x94\x80\x1b[5;47H \x08\x08\xe2\x94\x80\x1b[5;49H \x08\x08\xe2\x94\x80\x1b[5;51H \x08\x08\xe2\x94\x80\x1b[5;53H \x08\x08\xe2\x94\x80\x1b[5;55H \x08\x08\xe2\x94\x80\x1b[5;57H \x08\x08\xe2\x94\x80\x1b[5;59H \x08\x08\xe2\x94\x80\x1b[5;61H \x08\x08\xe2\x94\x80\x1b[5;63H \x08\x08\xe2\x94\x80\x1b[5;65H \x08\x08\xe2\x94\x80\x1b[5;67H \x08\x08\xe2\x94\x80\x1b[5;69H \x08\x08\xe2\x94\x80\x1b[5;71H \x08\x08\xe2\x94\x80\x1b[5;73H \x08\x08\xe2\x94\x80\x1b[5;75H \x08\x08\xe2\x94\x80\x1b[5;77H \x08\x08\xe2\x94\x90\x1b[5;79H\r\n \x08\x08\xe2\x94\x82\x1b[6;3H \xe6\x96\x87\xe7\xab\xa0\xe4\xbb\xa3\xe7\xa2\xbc(AID): \x1b[1;37m#13cPSYOX \x1b[m(Python) [ptt.cc] [\xe5\xbf\x83\xe5\xbe\x97] \xe7\xb5\x82\xe6\x96\xbc\xe9\x96\x8b\xe6\x9d\xbf\xe4\xba\x86\x1b[6;77H \x08\x08\xe2\x94\x82\x1b[6;79H\r\n \x08\x08\xe2\x94\x82\x1b[7;3H \xe6\x96\x87\xe7\xab\xa0\xe7\xb6\xb2\xe5\x9d\x80: \x1b[1;37mhttps://www.ptt.cc/bbs/Python/M.1134139170.A.621.html\x1b[7;77H \x08\x08\x1b[m\xe2\x94\x82\x1b[7;79H\r\n \x08\x08\xe2\x94\x82\x1b[8;3H \xe9\x80\x99\xe4\xb8\x80\xe7\xaf\x87\xe6\x96\x87\xe7\xab\xa0\xe5\x80\xbc 2 Ptt\xe5\xb9\xa3\x1b[8;77H \x08\x08\xe2\x94\x82\x1b[8;79H\r\n \x08\x08\xe2\x94\x94\x1b[9;3H \x08\x08\xe2\x94\x80\x1b[9;5H \x08\x08\xe2\x94\x80\x1b[9;7H \x08\x08\xe2\x94\x80\x1b[9;9H \x08\x08\xe2\x94\x80\x1b[9;11H \x08\x08\xe2\x94\x80\x1b[9;13H \x08\x08\xe2\x94\x80\x1b[9;15H \x08\x08\xe2\x94\x80\x1b[9;17H \x08\x08\xe2\x94\x80\x1b[9;19H \x08\x08\xe2\x94\x80\x1b[9;21H \x08\x08\xe2\x94\x80\x1b[9;23H \x08\x08\xe2\x94\x80\x1b[9;25H \x08\x08\xe2\x94\x80\x1b[9;27H \x08\x08\xe2\x94\x80\x1b[9;29H \x08\x08\xe2\x94\x80\x1b[9;31H \x08\x08\xe2\x94\x80\x1b[9;33H \x08\x08\xe2\x94\x80\x1b[9;35H \x08\x08\xe2\x94\x80\x1b[9;37H \x08\x08\xe2\x94\x80\x1b[9;39H \x08\x08\xe2\x94\x80\x1b[9;41H \x08\x08\xe2\x94\x80\x1b[9;43H \x08\x08\xe2\x94\x80\x1b[9;45H \x08\x08\xe2\x94\x80\x1b[9;47H \x08\x08\xe2\x94\x80\x1b[9;49H \x08\x08\xe2\x94\x80\x1b[9;51H \x08\x08\xe2\x94\x80\x1b[9;53H \x08\x08\xe2\x94\x80\x1b[9;55H \x08\x08\xe2\x94\x80\x1b[9;57H \x08\x08\xe2\x94\x80\x1b[9;59H \x08\x08\xe2\x94\x80\x1b[9;61H \x08\x08\xe2\x94\x80\x1b[9;63H \x08\x08\xe2\x94\x80\x1b[9;65H \x08\x08\xe2\x94\x80\x1b[9;67H \x08\x08\xe2\x94\x80\x1b[9;69H \x08\x08\xe2\x94\x80\x1b[9;71H \x08\x08\xe2\x94\x80\x1b[9;73H \x08\x08\xe2\x94\x80\x1b[9;75H \x08\x08\xe2\x94\x80\x1b[9;77H \x08\x08\xe2\x94\x98\x1b[9;79H\r\n\n\x1b[1;30m 8 12/10 Fenikso \x08\x08\xe2\x96\xa1\x1b[11;33H \xe8\xb3\x80 \r\n 9 12/10 asf423 \x08\x08\xe2\x96\xa1\x1b[12;33H \xe8\xb3\x80\xe9\x96\x8b\xe7\x89\x88 \r\n 10 12/10 rofu \x08\x08\xe2\x96\xa1\x1b[13;33H \xe8\xb3\x80\xe9\x96\x8b\xe7\x89\x88 \r\n 11 12/10 NewYork \x08\x08\xe2\x96\xa1\x1b[14;33H \xe8\xb3\x80 \r\n 12 12/10 abacada \x08\x08\xe2\x96\xa1\x1b[15;33H \xe6\x81\xad\xe5\x96\x9c\xe9\x96\x8b\xe6\x9d\xbf \r\n 13 12/10 jftsai \x08\x08\xe2\x96\xa1\x1b[16;33H \xe6\x81\xad\xe5\x96\x9c \r\n 14 12/11 aceace \x08\x08\xe2\x96\xa1\x1b[17;33H \xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe4\xbe\x86\xe4\xba\x86 \r\n 15 12/11 polaristin \x08\x08\xe2\x96\xa1\x1b[18;33H \xe8\xb3\x80\xe9\x96\x8b\xe6\x9d\xbf XD \r\n 16 12/11 jingel \x08\x08\xe2\x96\xa1\x1b[19;33H \xe6\x81\xad\xe5\x96\x9c\xe9\x96\x8b\xe6\x9d\xbf \r\n 17 12/11 milen \x08\x08\xe2\x96\xa1\x1b[20;33H \xe6\x81\xad\xe5\x96\x9c\xe9\x96\x8b\xe6\x9d\xbf!!~~^^~~ \r\n 18 12/12 littlebear \x08\x08\xe2\x96\xa1\x1b[21;33H \xe6\x81\xad\xe5\x96\x9c\xe9\x96\x8b\xe6\x9d\xbf...........^^^^ \r\n 19 12/12 zhouer \x08\x08\xe2\x96\xa1\x1b[22;33H [\xe9\x87\x8e\xe4\xba\xba\xe7\x8d\xbb\xe6\x9b\x9d] List Comprehensions \r\n 20 12/12 tippy \x08\x08\xe2\x96\xa1\x1b[23;33H [\xe5\xbf\x83\xe5\xbe\x97] httplib \r\n\x1b[34;44m \x08\x08\xe2\x96\x84\x1b[24;4H \x08\x08\xe2\x96\x84\x1b[24;6H \x08\x08\xe2\x96\x84\x1b[24;8H \x08\x08\xe2\x96\x84\x1b[24;10H \x08\x08\xe2\x96\x84\x1b[24;12H \x08\x08\xe2\x96\x84\x1b[24;14H \x08\x08\xe2\x96\x84\x1b[24;16H \x08\x08\xe2\x96\x84\x1b[24;18H \x08\x08\xe2\x96\x84\x1b[24;20H \x08\x08\xe2\x96\x84\x1b[24;22H \x08\x08\xe2\x96\x84\x1b[24;24H \x08\x08\xe2\x96\x84\x1b[24;26H \x08\x08\xe2\x96\x84\x1b[24;28H \x08\x08\xe2\x96\x84\x1b[24;30H \x08\x08\xe2\x96\x84\x1b[24;32H\x1b[0;1;37;44m \xe8\xab\x8b\xe6\x8c\x89\xe4\xbb\xbb\xe6\x84\x8f\xe9\x8d\xb5\xe7\xb9\xbc\xe7\xba\x8c \x08\x08\x1b[34m\xe2\x96\x84\x1b[24;50H \x08\x08\xe2\x96\x84\x1b[24;52H \x08\x08\xe2\x96\x84\x1b[24;54H \x08\x08\xe2\x96\x84\x1b[24;56H \x08\x08\xe2\x96\x84\x1b[24;58H \x08\x08\xe2\x96\x84\x1b[24;60H \x08\x08\xe2\x96\x84\x1b[24;62H \x08\x08\xe2\x96\x84\x1b[24;64H \x08\x08\xe2\x96\x84\x1b[24;66H \x08\x08\xe2\x96\x84\x1b[24;68H \x08\x08\xe2\x96\x84\x1b[24;70H \x08\x08\xe2\x96\x84\x1b[24;72H \x08\x08\xe2\x96\x84\x1b[24;74H \x08\x08\xe2\x96\x84\x1b[24;76H \x08\x08\xe2\x96\x84\x1b[24;78H \x1b[m' # test post screen = b'\x1b[H\x1b[2J\x1b[24;1H\n\x1b[K\x1b[H\n\xe5\xa6\x82\xe6\x9e\x9c\xe4\xbd\xa0\xe9\x82\x84\xe4\xb8\x8d\xe7\x9f\xa5\xe9\x81\x93\xe5\xa6\x82\xe4\xbd\x95\xe4\xbd\xbf\xe7\x94\xa8 Python \xe6\x93\x8d\xe4\xbd\x9c PTT\r\n\xe9\x82\xa3 PTT Library \xe6\x9c\x83\xe6\x98\xaf\xe4\xbd\xa0\xe4\xb8\x8d\xe9\x8c\xaf\xe7\x9a\x84\xe9\x81\xb8\xe6\x93\x87\xe3\x80\x82\r\n\nPTT Library \xe6\x95\xb4\xe7\x90\x86\xe4\xba\x86\xe7\xb6\xb2\xe8\xb7\xaf\xe4\xb8\x8a\xe8\xb7\x9f PTT \xe6\x89\x93\xe4\xba\xa4\xe9\x81\x93\xe7\x9a\x84\xe7\xa8\x8b\xe5\xbc\x8f\xe7\xa2\xbc\r\n\xe6\x9c\x9f\xe6\x9c\x9b\xe5\x9c\xa8 Python \xe5\x8f\xaf\xe4\xbb\xa5\xe6\x8f\x90\xe4\xbe\x9b\xe4\xb8\x80\xe5\x80\x8b\xe7\xa9\xa9\xe5\xae\x9a\xe5\xae\x8c\xe6\x95\xb4\xe7\x9a\x84\xe6\x9c\x8d\xe5\x8b\x99\r\n\n\xe5\xa6\x82\xe6\x9e\x9c\xe4\xbd\xa0\xe6\x9c\x89 PTT Library \xe5\xb0\x9a\xe6\x9c\xaa\xe6\x94\xb6\xe8\x97\x8f\xe7\x9a\x84\xe5\x8a\x9f\xe8\x83\xbd\r\n\xe6\xad\xa1\xe8\xbf\x8e\xe6\x8f\x90\xe5\x87\xba pull request :D\r\n\n\xe4\xbb\xa5\xe4\xb8\x8b\xe6\x98\xaf\xe6\x88\x91\xe6\x9c\x80\xe8\xbf\x91\xe6\x94\xb9\xe7\x89\x88\xe7\x9a\x84\xe9\x87\x8d\xe9\xbb\x9e\r\n\n1. \xe6\x9e\xb6\xe6\xa7\x8b\xe6\x89\x93\xe6\x8e\x89\xe9\x87\x8d\xe7\xb7\xb4\xef\xbc\x8c\xe4\xbb\xa5\xe6\x8f\x90\xe5\x8d\x87\xe9\x96\xb1\xe8\xae\x80\xe8\x88\x87\xe7\xb6\xad\xe8\xad\xb7\xe6\x80\xa7\xef\xbc\x8c\xe7\xac\xa6\xe5\x90\x88 PEP8 \xe9\xa2\xa8\xe6\xa0\xbc\xe8\xa6\x8f\xe7\xaf\x84\r\n2. \xe6\x94\xaf\xe6\x8f\xb4\xe6\x9c\x80\xe6\x96\xb0 WebSocket \xe9\x80\xa3\xe7\xb7\x9a\xe6\xa8\xa1\xe5\xbc\x8f\r\n3. \xe6\x94\xaf\xe6\x8f\xb4\xe5\xa4\x9a\xe5\x9c\x8b\xe8\xaa\x9e\xe7\xb3\xbb\xef\xbc\x8c\xe8\x8b\xb1\xe6\x96\x87\xe8\x88\x87\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87\r\n4. \xe6\x96\xbc Windows 10, Ubuntu 18.04 and MacOS 10.14 \xe6\xb8\xac\xe8\xa9\xa6\r\n5. \xe4\xbf\xae\xe6\xad\xa3\xe5\x9c\xa8\xe5\x89\x8d\xe4\xb8\x80\xe7\x89\x88\xe6\x94\xb6\xe9\x9b\x86\xe5\x88\xb0\xe7\x9a\x84\xe5\x95\x8f\xe9\xa1\x8c\r\n\ngithub: https://github.com/Truth0906/PTTLibrary\r\n\n\xe5\xa6\x82\xe6\x9e\x9c\xe4\xbd\xa0\xe5\x96\x9c\xe6\xad\xa1\xef\xbc\x8c\xe6\xad\xa1\xe8\xbf\x8e\xe7\xb5\xa6\xe6\x88\x91\xe5\x80\x8b\xe6\x98\x9f\xe6\x98\x9f :D\r\n--\r\n \x08\x08\x1b[32m\xe2\x80\xbb\x1b[23;3H \xe7\x99\xbc\xe4\xbf\xa1\xe7\xab\x99: \xe6\x89\xb9\xe8\xb8\xa2\xe8\xb8\xa2\xe5\xaf\xa6\xe6\xa5\xad\xe5\x9d\x8a(ptt.cc), \xe4\xbe\x86\xe8\x87\xaa: 111.243.146.98 (\xe8\x87\xba\xe7\x81\xa3)\r\n\x1b[34;46m \xe7\x80\x8f\xe8\xa6\xbd \xe7\xac\xac 1/2 \xe9\xa0\x81 ( 34%) \x1b[1;30;47m \xe7\x9b\xae\xe5\x89\x8d\xe9\xa1\xaf\xe7\xa4\xba: \xe7\xac\xac 06~28 \xe8\xa1\x8c\x1b[0;47m \x1b[31m(y)\x1b[30m\xe5\x9b\x9e\xe6\x87\x89\x1b[31m(X%)\x1b[30m\xe6\x8e\xa8\xe6\x96\x87\x1b[31m(h)\x1b[30m\xe8\xaa\xaa\xe6\x98\x8e\x1b[31m( \x08\x08\xe2\x86\x90\x1b[24;74H)\x1b[30m\xe9\x9b\xa2\xe9\x96\x8b \x1b[m' p = VT100Parser(screen, 'utf-8') print(p.screen) ================================================ FILE: PyPtt/service.py ================================================ import threading import time import uuid from typing import Optional from . import PTT from . import check_value from . import log class Service: def __init__(self, pyptt_init_config: Optional[dict] = None): """ 這是一個可以在多執行緒中使用的 PyPtt API 服務。 | 請注意:這僅僅只是 Thread Safe 的實作,對效能並不會有實質上的幫助。 | 如果你需要更好的效能,請在每一個線程都使用一個 PyPtt.API 本身。 Args: pyptt_init_config (dict): PyPtt 初始化設定,請參考 :ref:`初始化設定 `。 Returns: None 範例:: from PyPtt import Service def api_test(thread_id, service): result = service.call('get_time') print(f'thread id {thread_id}', 'get_time', result) result = service.call('get_aid_from_url', {'url': 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html'}) print(f'thread id {thread_id}', 'get_aid_from_url', result) result = service.call('get_newest_index', {'index_type': PyPtt.NewIndex.BOARD, 'board': 'Python'}) print(f'thread id {thread_id}', 'get_newest_index', result) if __name__ == '__main__': pyptt_init_config = { # 'language': PyPtt.Language.ENGLISH, } service = Service(pyptt_init_config) try: service.call('login', {'ptt_id': 'YOUR_PTT_ID', 'ptt_pw': 'YOUR_PTT_PW'}) pool = [] for i in range(10): t = threading.Thread(target=api_test, args=(i, service)) t.start() pool.append(t) for t in pool: t.join() service.call('logout') finally: service.close() """ if pyptt_init_config is None: pyptt_init_config = {} log_level = pyptt_init_config.get('log_level', log.INFO) self.logger = log.init(log_level, 'service') self.logger.info('init') self._api = None self._api_init_config = pyptt_init_config self._call_queue = [] self._call_result = {} self._id_pool = set() self._id_pool_lock = threading.Lock() self._close = False self._thread = threading.Thread(target=self._run, daemon=True) self._thread.start() while self._api is None: time.sleep(0.01) def _run(self): if self._api is not None: self._api.logout() self._api = None self._api = PTT.API(**self._api_init_config) self.logger.info('start') while not self._close: if len(self._call_queue) == 0: time.sleep(0.05) continue call = self._call_queue.pop(0) func = getattr(self._api, call['api']) api_result = None api_exception = None try: api_result = func(**call['args']) except Exception as e: api_exception = e self._call_result[call['id']] = { 'result': api_result, 'exception': api_exception } def _get_call_id(self): while True: call_id = uuid.uuid4().hex with self._id_pool_lock: if call_id not in self._id_pool: self._id_pool.add(call_id) return call_id def call(self, api: str, args: Optional[dict] = None): if args is None: args = {} check_value.check_type(api, str, 'api') check_value.check_type(args, dict, 'args') if api not in dir(self._api): raise ValueError(f'api {api} not found') call = { 'api': api, 'id': self._get_call_id(), 'args': args } self._call_queue.append(call) while call['id'] not in self._call_result: time.sleep(0.01) call_result = self._call_result[call['id']] del self._call_result[call['id']] with self._id_pool_lock: self._id_pool.remove(call['id']) if call_result['exception'] is not None: raise call_result['exception'] return call_result['result'] def close(self): self.logger.info('close') self._close = True self._thread.join() self.logger.info('done') ================================================ FILE: README.md ================================================ ![](https://raw.githubusercontent.com/PttCodingMan/PyPtt/master/logo/facebook_cover_photo_2.png) # PyPtt [![Package Version](https://img.shields.io/pypi/v/PyPtt.svg)](https://pypi.python.org/pypi/PyPtt) ![PyPI - Downloads](https://img.shields.io/pypi/dm/PyPtt) [![test](https://github.com/PyPtt/PyPtt/actions/workflows/test.yml/badge.svg)](https://github.com/PyPtt/PyPtt/actions/workflows/test.yml) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/PyPtt) [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) [![chatroom icon](https://patrolavia.github.io/telegram-badge/chat.png)](https://t.me/PyPtt) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](http://paypal.me/CodingMan) #### PyPtt (PTT Library) 是一套 Pure Python PTT API 是目前支援最完整的 PTT API。具備大部分常用功能,無論推文、發文、取得文章、取得信件、寄信、發 P 幣、丟水球,你都可以在這裡找到完整的使用範例 #### 使用帳號登入,支援使用登入之後才可以使用的功能,例如:推文、發文、寄信、發 P 幣等等 #### 本專案意旨在提供 PTT 自動化機器人函式庫,無意違反任何 PTT 站方規範。如有牴觸,請馬上告知。 #### #### Pypi: https://pypi.org/project/PyPtt/ ## 安裝 ```bash pip install PyPtt ``` ## 回報問題 #### 請參考 [常見問題](https://pyptt.cc/faq.html) 章節 ## 加入 PyPtt 社群 #### 你可以在 Telegram 上找到 PyPtt 社群 [![chatroom icon](https://patrolavia.github.io/telegram-badge/chat.png)](https://t.me/PyPtt) ## 贊助 #### 如果這個專案對你有幫助,贊助我一杯咖啡吧!! #### #### [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](http://paypal.me/CodingMan) ## 贊助清單 #### leftc ================================================ FILE: docs/CNAME ================================================ pyptt.cc ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: docs/api/bucket.rst ================================================ bucket ========== .. _api-bucket: .. automodule:: PyPtt.API :members: bucket :noindex: ================================================ FILE: docs/api/change_pw.rst ================================================ change_pw ============== .. automodule:: PyPtt.API :members: change_pw :noindex: ================================================ FILE: docs/api/comment.rst ================================================ comment ========== .. _api-comment: .. automodule:: PyPtt.API :members: comment :noindex: ================================================ FILE: docs/api/del_mail.rst ================================================ del_mail ============ .. _api-del-mail: .. automodule:: PyPtt.API :members: del_mail :noindex: ================================================ FILE: docs/api/del_post.rst ================================================ del_post ============== .. automodule:: PyPtt.API :members: del_post :noindex: ================================================ FILE: docs/api/get_aid_from_url.rst ================================================ get_aid_from_url ===================== .. automodule:: PyPtt.API :members: get_aid_from_url :noindex: ================================================ FILE: docs/api/get_all_boards.rst ================================================ get_all_boards ================= .. automodule:: PyPtt.API :members: get_all_boards :noindex: ================================================ FILE: docs/api/get_board_info.rst ================================================ get_board_info ================= .. _api-get-board-info: .. automodule:: PyPtt.API :members: get_board_info :noindex: ================================================ FILE: docs/api/get_bottom_post_list.rst ================================================ get_bottom_post_list ====================== .. automodule:: PyPtt.API :members: get_bottom_post_list :noindex: ================================================ FILE: docs/api/get_favourite_boards.rst ================================================ get_favourite_boards ========================== .. _api-get-favourite-boards: .. automodule:: PyPtt.API :members: get_favourite_boards :noindex: ================================================ FILE: docs/api/get_mail.rst ================================================ get_mail ============ .. _api-get-mail: .. automodule:: PyPtt.API :members: get_mail :noindex: ================================================ FILE: docs/api/get_newest_index.rst ================================================ get_newest_index ================= .. _api-get-newest-index: .. automodule:: PyPtt.API :members: get_newest_index :noindex: ================================================ FILE: docs/api/get_post.rst ================================================ get_post ========== .. _api-get-post: .. automodule:: PyPtt.API :members: get_post :noindex: ================================================ FILE: docs/api/get_time.rst ================================================ get_time ========== .. _api-get-time: .. automodule:: PyPtt.API :members: get_time :noindex: ================================================ FILE: docs/api/get_user.rst ================================================ get_user ========== .. _api-get-user: .. automodule:: PyPtt.API :members: get_user :noindex: ================================================ FILE: docs/api/give_money.rst ================================================ give_money ============= .. _api-give-money: .. automodule:: PyPtt.API :members: give_money :noindex: ================================================ FILE: docs/api/index.rst ================================================ APIs ============= | 這是 PyPtt 的 API 文件。 | 我們在這裡介紹 PyPtt 目前所有支援 PTT, PTT2 的功能。 基本功能 ---------------- .. toctree:: init login_logout 文章相關 ---------------- .. toctree:: get_post get_newest_index post reply_post del_post comment 信箱相關 ---------------- .. toctree:: mail get_mail del_mail 使用者相關 ---------------- .. toctree:: give_money get_user search_user change_pw 取得 PTT 資訊 ------------------- .. toctree:: get_time get_all_boards get_favourite_boards get_board_info get_aid_from_url get_bottom_post_list 版主相關 ---------------- .. toctree:: set_board_title mark_post bucket ================================================ FILE: docs/api/init.rst ================================================ init ======= .. _api-init: .. automodule:: PyPtt.API :members: __init__ ================================================ FILE: docs/api/login_logout.rst ================================================ login, logout ================ .. _api-login-logout: .. automodule:: PyPtt.API :members: login, logout :noindex: ================================================ FILE: docs/api/mail.rst ================================================ mail ============= .. _api-mail: .. automodule:: PyPtt.API :members: mail :noindex: ================================================ FILE: docs/api/mark_post.rst ================================================ mark_post =============== .. _api-mark-post: .. automodule:: PyPtt.API :members: mark_post :noindex: ================================================ FILE: docs/api/post.rst ================================================ post ========== .. _api-post: .. automodule:: PyPtt.API :members: post :noindex: ================================================ FILE: docs/api/reply_post.rst ================================================ reply_post ========== .. _api-reply-post: .. automodule:: PyPtt.API :members: reply_post :noindex: ================================================ FILE: docs/api/search_user.rst ================================================ search_user ================ .. _api-search-user: .. automodule:: PyPtt.API :members: search_user :noindex: ================================================ FILE: docs/api/set_board_title.rst ================================================ set_board_title ===================== .. _api-set-board-title: .. automodule:: PyPtt.API :members: set_board_title :noindex: ================================================ FILE: docs/changelog.rst ================================================ 更新日誌 ==================== | 這裡寫著 PyPtt 的故事。 | | 2022.12.20 PyPtt 1.0.3,logger 改採用以 logging_ 為基底。 .. _logging: https://docs.python.org/3/howto/logging.html | 2022.12.19 發佈 :doc:`Docker Image `。 | 2022.12.08 PyPtt 1.0.1, 1.0.2,修正一些小錯誤 | 2022.12.08 PyPtt 1.0.0 正式發布。 | 2021.12.08 PyPtt 新增 :doc:`service` 功能。 | 2022.12.01 開發 頁面改名為 Roadmap。 | 2022.09.19 更換主題為 furo_。 .. _furo: https://sphinx-themes.org/sample-sites/furo/ | 2022.09.14 太棒了!我們終於有更新日誌了。 ================================================ FILE: docs/conf.py ================================================ # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information import os import sys from datetime import datetime sys.path.insert(0, os.path.abspath('../')) import PyPtt project = 'PyPtt' copyright = f'2017 ~ {datetime.now().year}, CodingMan' author = 'CodingMan' version = PyPtt.__version__ release = PyPtt.__version__ html_title = f'PyPtt.cc' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx_copybutton', 'sphinx.ext.autosectionlabel', 'sphinx_sitemap', ] autosectionlabel_prefix_document = True html_baseurl = 'https://pyptt.cc/' sitemap_url_scheme = "{link}" templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] language = 'zh_TW' html_extra_path = ['CNAME', 'robots.txt'] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = 'furo' html_static_path = ['_static'] html_favicon = "https://raw.githubusercontent.com/PyPtt/PyPtt/master/logo/facebook_profile_image.png" ================================================ FILE: docs/dev.rst ================================================ Development ================ 如果你想參與開發,請參考以下須知: 開發環境 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 我們建議您使用 virtualenv 來建立獨立的 Python 環境,以避免相依性問題。 .. code-block:: bash virtualenv venv source venv/bin/activate 安裝相依套件 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 你可以使用以下指令來安裝相依套件: .. code-block:: bash pip install -r requirements.txt 如果你想更改文件,請安裝開發相依套件: .. code-block:: bash pip install -r docs/requirements.txt 產生文件網頁 .. code-block:: bash bash make_doc.sh 你可以在 docs/_build/html/index.html 中查看文件網頁。 執行測試 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 你可以使用以下指令來執行測試: .. code-block:: python python3 tests/*.py 如果有遺漏的測試,請不吝發起 Pull Request。 撰寫文件 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 如果你的變更涉及文件,請記得更新文件。 | 我們使用 Sphinx 來撰寫文件,你可以在 docs/ 中找到文件的原始碼。 建立你的 Pull Request ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 如果你想要貢獻程式碼,請參考以下步驟: 1. Fork 這個專案。 2. 建立你的特性分支 (`git checkout -b feat/my-new-feature`)。 3. Commit 你的變更 (`git commit -am 'feat: add some feature`)。 commit msg 格式,請參考 `Conventional Commits`_。 4. Push 到你的分支 (`git push origin feat/my-new-feature`)。 5. 建立一個新的 Pull Request。 請注意,我們會優先處理符合 `Conventional Commits`_ 的 Pull Request。 .. _Conventional Commits: https://www.conventionalcommits.org/en/v1.0.0/ ================================================ FILE: docs/docker.rst ================================================ Docker Image ================= .. image:: https://img.shields.io/docker/v/codingman000/pyptt/latest :target: https://hub.docker.com/r/codingman000/pyptt .. image:: https://img.shields.io/docker/pulls/codingman000/pyptt?color=orange :target: https://hub.docker.com/r/codingman000/pyptt .. image:: https://img.shields.io/docker/image-size/codingman000/pyptt/latest?color=green :target: https://hub.docker.com/r/codingman000/pyptt .. image:: https://img.shields.io/docker/stars/codingman000/pyptt?color=succes :target: https://hub.docker.com/r/codingman000/pyptt | 是的,PyPtt 也支援 Docker Image。 | 只要一行指令就可以啟動一個 PyPtt 的 Docker Image,並且可以在 Docker Image 中使用 PyPtt。 | | Doc: https://pyptt.cc/docker.html | Docker hub: https://hub.docker.com/r/codingman000/pyptt | Github: https://github.com/PyPtt/PyPtt_image 安裝 ----------------- .. code-block:: bash docker pull codingman000/pyptt:latest 啟動 ----------------- .. code-block:: bash docker run -d -p 8787:8787 codingman000/pyptt:latest 連線 ----------------- 物件編碼的方法你可以在這裏了解 程式碼_ .. _程式碼: https://github.com/PyPtt/PyPtt_image/blob/main/src/utils.py#L4 .. code-block:: python import PyPtt import requests from src.utils import object_encode from tests import config if __name__ == '__main__': params = { "api": "login", "args": object_encode({ 'ptt_id': config.PTT_ID, 'ptt_pw': config.PTT_PW }) } r = requests.get("http://localhost:8787/api", params=params) print(r.json()) params = { "api": "get_time", } r = requests.get("http://localhost:8787/api", params=params) print(r.json()) params = { "api": "get_newest_index", "args": object_encode({ 'board': 'Gossiping', 'index_type': PyPtt.NewIndex.BOARD }) } r = requests.get("http://localhost:8787/api", params=params) print(r.json()) ############################## content = """此內容由 PyPtt image 執行 PO 文 測試換行 123 測試換行 456 測試換行 789 """ params = { "api": "post", "args": object_encode({ 'board': 'Test', 'title_index': 1, 'title': 'test', 'content': content, }) } r = requests.get("http://localhost:8787/api", params=params) print(r.json()) ############################## params = { "api": "logout", } r = requests.get("http://localhost:8787/api", params=params) print(r.json()) ================================================ FILE: docs/examples.rst ================================================ 使用範例 ============= | 這裡記錄了各種實際使用的範例 ☺️ 保持登入 -------- 這裡示範了如何保持登入 .. code-block:: python import PyPtt def login(): max_retry = 5 ptt_bot = None for retry_time in range(max_retry): try: ptt_bot = PyPtt.API() ptt_bot.login('YOUR_ID', 'YOUR_PW', kick_other_session=False if retry_time == 0 else True) break except PyPtt.exceptions.LoginError: ptt_bot = None print('登入失敗') time.sleep(3) except PyPtt.exceptions.LoginTooOften: ptt_bot = None print('請稍後再試') time.sleep(60) except PyPtt.exceptions.WrongIDorPassword: print('帳號密碼錯誤') raise except Exception as e: print('其他錯誤:', e) break return ptt_bot if __name__ == '__main__': login() last_newest_index = ptt_bot.get_newest_index() time.sleep(60) try: while True: try: newest_index = ptt_bot.get_newest_index() except PyPtt.exceptions.ConnectionClosed: ptt_bot = login() continue except Exception as e: print('其他錯誤:', e) break if newest_index == last_newest_index: continue print('有新文章!', newest_index) # do something time.sleep(5) finally: ptt_bot.logout() 幫你的文章上色 -------------- 如果在發的時候有上色的需求,可以透過模擬鍵盤輸入的方式達到加上色碼的效果 .. code-block:: python import PyPtt content = [ PTT.command.Ctrl_C + PTT.command.Left + '5' + PTT.command.Right + '這是閃爍字' + PTT.command.Ctrl_C, PTT.command.Ctrl_C + PTT.command.Left + '31' + PTT.command.Right + '前景紅色' + PTT.command.Ctrl_C, PTT.command.Ctrl_C + PTT.command.Left + '44' + PTT.command.Right + '背景藍色' + PTT.command.Ctrl_C, ] content = '\n'.join(content) ptt_bot = PyPtt.API() try: # .. login .. ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content=content, sign_file=0) finally: ptt_bot.logout() .. image:: _static/color_demo.png .. _check_post_status: 如何判斷文章資料是否可以使用 ------------------------------ 當 :doc:`api/get_post` 回傳文章資料回來時,這時需要一些判斷來決定是否要使用這些資料 .. code-block:: python import PyPtt ptt_bot = PyPtt.API() try: # .. login .. post_info = ptt_bot.get_post('Python', index=1) print(post_info) if post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.EXISTS: print('文章存在!') elif post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.DELETED_BY_AUTHOR: print('文章被作者刪除') sys.exit() elif post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.DELETED_BY_MODERATOR: print('文章被版主刪除') sys.exit() if not post_info[PyPtt.PostField.pass_format_check]: print('未通過格式檢查') sys.exit() print('文章資料可以使用') finally: ptt_bot.logout() ================================================ FILE: docs/exceptions.rst ================================================ 例外 ======== | 這裡介紹 PyPtt 的例外。 | 可以用 try...except... 來處理。 | 例外的種類 .. py:exception:: PyPtt.exceptions.RequireLogin :module: PyPtt 需要登入。 .. py:exception:: PyPtt.exceptions.NoPermission :module: PyPtt 沒有權限。 .. py:exception:: PyPtt.exceptions.LoginError :module: PyPtt 登入失敗。 .. py:exception:: PyPtt.exceptions.NoFastComment :module: PyPtt 無法快速推文。 .. py:exception:: PyPtt.exceptions.NoSuchUser :module: PyPtt 查無此使用者。 .. py:exception:: PyPtt.exceptions.NoSuchMail :module: PyPtt 查無此信件。 .. py:exception:: PyPtt.exceptions.NoMoney :module: PyPtt 餘額不足。 .. py:exception:: PyPtt.exceptions.NoSuchBoard :module: PyPtt 查無此看板。 .. py:exception:: PyPtt.exceptions.ConnectionClosed :module: PyPtt 連線已關閉。 .. py:exception:: PyPtt.exceptions.UnregisteredUser :module: PyPtt 未註冊使用者。 .. py:exception:: PyPtt.exceptions.MultiThreadOperated :module: PyPtt 同時使用多個 thread 呼叫 PyPtt 。 .. py:exception:: PyPtt.exceptions.WrongIDorPassword :module: PyPtt 帳號或密碼錯誤。 .. py:exception:: PyPtt.exceptions.WrongPassword :module: PyPtt 密碼錯誤。 .. py:exception:: PyPtt.exceptions.LoginTooOften :module: PyPtt 登入太頻繁。 .. py:exception:: PyPtt.exceptions.UseTooManyResources :module: PyPtt 使用過多資源。 .. py:exception:: PyPtt.exceptions.HostNotSupport :module: PyPtt 主機不支援。詳見 :ref:`host`。 .. py:exception:: PyPtt.exceptions.CantComment :module: PyPtt 禁止推文。 .. py:exception:: PyPtt.exceptions.CantResponse :module: PyPtt 已結案並標記, 不得回應。 .. py:exception:: PyPtt.exceptions.NeedModeratorPermission :module: PyPtt 需要版主權限。 .. py:exception:: PyPtt.exceptions.ConnectError :module: PyPtt 連線失敗。 .. py:exception:: PyPtt.exceptions.NoSuchPost :module: PyPtt 文章不存在。 .. py:exception:: PyPtt.exceptions.CanNotUseSearchPostCode :module: PyPtt 無法使用搜尋文章代碼。 .. py:exception:: PyPtt.exceptions.UserHasPreviouslyBeenBanned :module: PyPtt `水桶`_ 使用者,但已經被 `水桶`_。 .. py:exception:: PyPtt.exceptions.MailboxFull :module: PyPtt 信箱已滿。 .. py:exception:: PyPtt.exceptions.NoSearchResult :module: PyPtt 搜尋結果為空。 .. py:exception:: PyPtt.exceptions.OnlySecureConnection :module: PyPtt 只能使用安全連線。 .. py:exception:: PyPtt.exceptions.SetContactMailFirst :module: PyPtt 請先設定聯絡信箱。 .. py:exception:: PyPtt.exceptions.ResetYourContactEmail :module: PyPtt 請重新設定聯絡信箱。 .. _水桶: https://pttpedia.fandom.com/zh/wiki/%E6%B0%B4%E6%A1%B6 ================================================ FILE: docs/faq.rst ================================================ FAQ ========== 這裡搜集了一些常見問題的解答,如果你有任何問題,請先看看這裡。 Q: 我該如何使用 PyPtt? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | A: 可以先參考 :doc:`install`、:doc:`api/index` 與 :doc:`examples`。 Q: 使用 PyPtt 時,遇到問題該如何解決? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. 自己修正並提交 PR,如果沒辦法請參考步驟 2。 * 如果你是程式設計師,可以參考 :doc:`參與開發 ` 一起幫忙修正問題。 2. 到 GitHub 提出 `issue`_ 或者到 `PyPtt Telegram 社群`_ 討論。 * 請先確認你使用的版本是否為 |version_pic|,如果不是,請更新到最新版本。 * 如果你使用的是最新版本,請確認你的問題是否已經在這裡被回答過了。 * 如果你的問題還沒有被回答過,請依照以下程式碼將 LogLevel_ 設定為 `DEBUG`,並附上 **可以重現問題的程式碼**。 * 到 GitHub 提出 `issue`_ 或者到 `PyPtt Telegram 社群`_ 討論。 .. code-block:: python import PyPtt ptt_bot = PyPtt.API(log_level=PyPtt.LogLevel.DEBUG) # 你的程式碼 .. |version_pic| image:: https://img.shields.io/pypi/v/PyPtt.svg :target: https://pypi.org/project/PyPtt/ .. _`PyPtt Telegram 社群`: https://t.me/PyPtt .. _LogLevel: https://github.com/PttCodingMan/SingleLog/blob/d7c19a1b848dfb1c9df8201f13def9a31afd035c/SingleLog/SingleLog.py#L22 .. _`issue`: https://github.com/PyPtt/PyPtt/issues/new Q: 在 jupyter 遭遇 `the event loop is already running` 錯誤 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | A: 因為 jupyter 內部也使用了 asyncio 作為協程管理工具,會跟 PyPtt 內部的 asyncio 衝突,所以如果想要在 jypyter 內使用,請在你的程式碼中加入以下程式碼 .. code-block:: bash :caption: 安裝 nest_asyncio ! pip install nest_asyncio .. code-block:: python :caption: 在程式碼中引用 nest_asyncio import nest_asyncio nest_asyncio.apply() Q: 在 Mac 無法使用 WebSocket 連線,遭遇 SSL 相關錯誤 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | A: 請參考以下指令,安裝 Python 的 SSL 憑證 .. code-block:: bash :caption: 以 Python 3.10 為例 sh /Applications/Python\ 3.10/Install\ Certificates.command Q: 為什麼我沒辦法在雲端環境上使用 PyPtt? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | A: 如果你是使用雲端 (Colab、GCP、Azure、AWS...etc) 無法連線 PTT 是正常的。 | 因為 PTT 有防止機器人登入的機制,所以在雲端環境上無法使用 PyPtt。 ================================================ FILE: docs/index.rst ================================================ PyPtt ==================== .. image:: _static/logo_cover.png :alt: PyPtt: PTT bot library for Python :align: center .. image:: https://img.shields.io/pypi/v/PyPtt.svg :target: https://pypi.org/project/PyPtt/ .. image:: https://img.shields.io/github/last-commit/pyptt/pyptt.svg?color=green :target: https://github.com/PyPtt/PyPtt/commits/ .. image:: https://img.shields.io/pypi/dm/PyPtt?color=ocean :target: https://pypi.org/project/PyPtt/ .. image:: https://github.com/PyPtt/PyPtt/actions/workflows/test.yml/badge.svg?branch=master&color=yellogreen :target: https://github.com/PyPtt/PyPtt/actions/workflows/test.yml .. image:: https://img.shields.io/pypi/pyversions/PyPtt :target: https://pypi.org/project/PyPtt/ .. image:: https://img.shields.io/badge/License-LGPL%20v3-blue.svg :target: https://www.gnu.org/licenses/lgpl-3.0 .. image:: https://img.shields.io/github/stars/pyptt/pyptt?style=social :target: https://github.com/PyPtt/PyPtt/stargazers | PyPtt_ 是時下最流行的 PTT library,你可以在 Python 程式碼裡面使用 PTT 常見的操作,例如::doc:`推文 `、:doc:`發文 `、:doc:`寄信 `、:doc:`讀取信件 `、:doc:`讀取文章 ` 等等操作。 | | 本文件的內容會隨著 PyPtt_ 的更新而更新,如果你發現任何錯誤,歡迎到 PyPtt_ 發 issue 或者加入 `PyPtt Telegram 社群`_ 一起討論。 | | PyPtt 由 CodingMan_ 與其他許多的 `貢獻者`_ 共同維護。 .. _PyPtt: https://github.com/PyPtt/PyPtt .. _`PyPtt Telegram 社群`: https://t.me/PyPtt .. _CodingMan: https://github.com/PttCodingMan .. _`貢獻者`: https://github.com/PyPtt/PyPtt/graphs/contributors 重要消息 -------------------- | 2022.12.19 發佈 :doc:`Docker Image `。 | 2022.12.08 PyPtt 1.0.0 正式發布 | 2021.12.08 PyPtt 新增 :doc:`service` 功能 文件 ---------------- :doc:`安裝 PyPtt ` 如何把 PyPtt 安裝到你的環境中。 :doc:`APIs ` PyPtt 的所有 API 說明。 :doc:`Service ` 如何在多線程的情況,安全地使用 PyPtt。 :doc:`參數型態 ` PyPtt 的所有參數型態選項。 :doc:`例外 ` PyPtt 所有你可能遭遇到的錯誤。 :doc:`使用範例 ` 一些使用 PyPtt 的範例。 :doc:`Docker Image ` 如何使用 Docker Image 來使用 PyPtt。 :doc:`參與開發 ` 如果你想要貢獻 PyPtt,可以看看這裡。 :doc:`常見問題 ` 任何常見問題都可以在這找到解答。 :doc:`Roadmap ` | 這裡列了我們正在做什麼與打算做什麼。 | 如果你想要貢獻 PyPtt,可以看看這裡。 :doc:`ChangeLog ` | 我們曾經做了什麼。 .. toctree:: :maxdepth: 3 :caption: 目錄 :hidden: install api/index service type exceptions examples docker image 參與開發 常見問題 Roadmap ChangeLog Source Code PyPI ================================================ FILE: docs/install.rst ================================================ 安裝 PyPtt =================== Python 版本 -------------- | 推薦使用 CPython_ 3.8+。 .. _CPython: https://www.python.org/ 相依套件 -------------- PyPtt 目前相依於以下套件,這些套件都會在安裝的過程中被自動安裝。 * progressbar2_ is a text progress bar library for Python. * websockets_ is a library for building WebSocket_ servers and clients in Python with a focus on correctness, simplicity, robustness, and performance. * uao_ is a pure Python implementation of the Unicode encoder/decoder. * requests_ is a Python HTTP library, released under the Apache License 2.0. * AutoStrEnum_ is a Python library that provides an Enum class that automatically converts enum values to and from strings. * PyYAML_ is a YAML parser and emitter for Python. .. _progressbar2: https://progressbar-2.readthedocs.io/en/latest/ .. _websockets: https://websockets.readthedocs.io/en/stable/ .. _`WebSocket`: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API .. _uao: https://github.com/eight04/pyUAO .. _requests: https://requests.readthedocs.io/en/master/ .. _AutoStrEnum: https://github.com/PttCodingMan/PttCodingMan .. _PyYAML: https://pyyaml.org/ 使用虛擬環境安裝 (推薦) ------------------------- | 我們推薦各位使用虛擬環境 venv_ 來安裝 PyPtt,因為可以盡可能地避免套件衝突。 | | 你可以從 `Virtual Environments and Packages`_ 中了解,更多關於使用虛擬環境的理由以及如何建立你的虛擬環境。 .. _`Virtual Environments and Packages`: https://docs.python.org/3/tutorial/venv.html#tut-venv .. _venv: https://docs.python.org/3/library/venv.html 安裝指令 ---------------- 你可以使用以下指令來安裝 PyPtt。 .. code-block:: bash pip install PyPtt 現在 PyPtt 已經成功安裝了,來看看 PyPtt 的 :doc:`API 說明 ` 或者 :doc:`使用範例 ` 吧! ================================================ FILE: docs/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd ================================================ FILE: docs/requirements.txt ================================================ PyPtt sphinx sphinx-copybutton pygments==2.15.0 Furo sphinx-sitemap ================================================ FILE: docs/roadmap.rst ================================================ 開發 ============= | 這裡列了一些我們正在開發的功能,如果你有任何建議,歡迎找我們聊聊。 | 或者你也可以直接在 github 上開 issue,我們會盡快回覆。 | 當然如果你有興趣參與開發,也歡迎你加入我們,我們會盡快回覆你的加入申請。 未來開發計劃 -------------------- * WebSocket 支援 Tor or Proxy 期待有一天可以透過 Tor 或 Proxy 來連接到 PTT,讓 PyPtt 可以自由地在雲端伺服器運作。 開發中 -------------------- * 支援 PTT 官方 APP API 你可以在 ptt-app-api_ 分支上找到目前的進度。 .. _ptt-app-api: https://github.com/PyPtt/PyPtt/tree/ptt-app-api 已完成 -------------------- * PyPtt Service docker | 期待 PyPtt 在未來可以有 API 形式的服務,讓大家可以透過 API 呼叫來使用 PyPtt。 | 這樣其實在某個層面也上可以達到使用 Tor or Proxy 的目的。 * PyPtt :doc:`service` 2022.12.18 完成 * Docker :doc:`docker` 2022.12.19 完成 * 官方網站的建置 2022.12.18 完成 使用 sphinx 來建置官方網站,讓大家可以更方便地了解 PyPtt。 * 測試案例 2022.12.15 完成 * 1.0 正式版本重構 2022.11.15 完成 ================================================ FILE: docs/robots.txt ================================================ User-agent: * Sitemap: https://pyptt.cc/sitemap.xml ================================================ FILE: docs/service.rst ================================================ Service =========== .. automodule:: PyPtt.Service :members: __init__ :undoc-members: ================================================ FILE: docs/type.rst ================================================ 參數型態 =========== 這裡介紹 PyPtt 的參數型態 .. _host: HOST ----------- * 連線的 PTT 伺服器。 .. py:attribute:: PyPtt.HOST.PTT1 批踢踢實業坊 .. py:attribute:: PyPtt.HOST.PTT2 批踢踢兔 .. _language: Language ----------- * 顯示訊息的語言。 .. py:attribute:: PyPtt.Language.MANDARIN 繁體中文 .. py:attribute:: PyPtt.Language.ENGLISH 英文 .. _connect-mode: ConnectMode ----------- * 連線的模式。 .. py:attribute:: PyPtt.ConnectMode.WEBSOCKETS 使用 WEBSOCKETS 連線 .. py:attribute:: PyPtt.ConnectMode.TELNET 使用 TELNET 連線 .. _new-index: NewIndex ----------- * 搜尋 Index 的種類。 .. py:attribute:: PyPtt.NewIndex.BOARD 搜尋看板 Index .. py:attribute:: PyPtt.NewIndex.MAIL 搜尋信箱 Index .. _search-type: SearchType ----------- * 搜尋看板的方式。 .. py:attribute:: PyPtt.SearchType.KEYWORD 搜尋關鍵字 .. py:attribute:: PyPtt.SearchType.AUTHOR 搜尋作者 .. py:attribute:: PyPtt.SearchType.COMMENT 搜尋推文數 .. py:attribute:: PyPtt.SearchType.MARK 搜尋標記 .. py:attribute:: PyPtt.SearchType.MONEY 搜尋稿酬 .. _reply-to: ReplyTo ----------- * 回文的方式。 .. py:attribute:: PyPtt.ReplyTo.BOARD 回文至看板 .. py:attribute:: PyPtt.ReplyTo.MAIL 回文至信箱 .. py:attribute:: PyPtt.ReplyTo.BOARD_MAIL 回文至看板與信箱 .. _comment-type: CommentType ----------- * 推文方式。 .. py:attribute:: PyPtt.CommentType.PUSH 推 .. py:attribute:: PyPtt.CommentType.BOO 噓 .. py:attribute:: PyPtt.CommentType.ARROW 箭頭 .. _post-status: PostStatus ----------- * 文章狀態。 .. py:attribute:: PyPtt.PostStatus.EXISTS 文章存在 .. py:attribute:: PyPtt.PostStatus.DELETED_BY_AUTHOR 被作者刪除 .. py:attribute:: PyPtt.PostStatus.DELETED_BY_MODERATOR 被板主刪除 .. py:attribute:: PyPtt.PostStatus.DELETED_BY_UNKNOWN 無法判斷,被如何刪除 .. _mark-type: MarkType ----------- * 版主標記文章種類 .. py:attribute:: PyPtt.MarkType.S S 文章 .. py:attribute:: PyPtt.MarkType.D 標記文章 .. py:attribute:: PyPtt.MarkType.DELETE_D 刪除標記文章 .. py:attribute:: PyPtt.MarkType.M M 起來 .. py:attribute:: PyPtt.MarkType.UNCONFIRMED 待證實文章 .. _user-field: UserField ----------- * 使用者資料欄位。 .. py:attribute:: PyPtt.UserField.ptt_id 使用者 ID .. py:attribute:: PyPtt.UserField.money 經濟狀態 .. py:attribute:: PyPtt.UserField.login_count 登入次數 .. py:attribute:: PyPtt.UserField.account_verified 是否通過認證 .. py:attribute:: PyPtt.UserField.legal_post 文章數量 .. py:attribute:: PyPtt.UserField.illegal_post 退文數量 .. py:attribute:: PyPtt.UserField.activity 目前動態 .. py:attribute:: PyPtt.UserField.mail 信箱狀態 .. py:attribute:: PyPtt.UserField.last_login_date 最後登入時間 .. py:attribute:: PyPtt.UserField.last_login_ip 最後登入 IP .. py:attribute:: PyPtt.UserField.five_chess 五子棋戰積 .. py:attribute:: PyPtt.UserField.chess 象棋戰積 .. py:attribute:: PyPtt.UserField.signature_file 簽名檔 .. _comment-field: CommentField -------------- * 推文資料欄位。 .. py:attribute:: PyPtt.CommentField.type 推文類型,推噓箭頭,詳見 :ref:`comment-type` .. py:attribute:: PyPtt.CommentField.author 推文作者 .. py:attribute:: PyPtt.CommentField.content 推文內容 .. py:attribute:: PyPtt.CommentField.ip 推文 IP (如果存在) .. py:attribute:: PyPtt.CommentField.time 推文時間 .. _favorite-board-field: FavouriteBoardField -------------------- * 我的最愛資料欄位。 .. py:attribute:: PyPtt.FavouriteBoardField.board 看板名稱 .. py:attribute:: PyPtt.FavouriteBoardField.title 看板標題 .. py:attribute:: PyPtt.FavouriteBoardField.type 類別 .. _mail-field: MailField ---------- * 信件資料欄位。 .. py:attribute:: PyPtt.MailField.origin_mail 原始信件全文 .. py:attribute:: PyPtt.MailField.author 信件作者 .. py:attribute:: PyPtt.MailField.title 信件標題 .. py:attribute:: PyPtt.MailField.date 信件日期 .. py:attribute:: PyPtt.MailField.content 信件內容 .. py:attribute:: PyPtt.MailField.ip 信件 IP .. py:attribute:: PyPtt.MailField.location 信件位置 .. py:attribute:: PyPtt.MailField.is_red_envelope 是否為紅包 .. _board-field: BoardField ----------- * 看板資料欄位。 .. py:attribute:: PyPtt.BoardField.board 看板名稱 .. py:attribute:: PyPtt.BoardField.online_user 在線人數 .. py:attribute:: PyPtt.BoardField.chinese_des 看板中文名稱 .. py:attribute:: PyPtt.BoardField.moderators 看板板主清單 .. py:attribute:: PyPtt.BoardField.open_status 看板公開狀態,是否隱板 .. py:attribute:: PyPtt.BoardField.into_top_ten_when_hide 隱板時是否可以進入十大排行榜 .. py:attribute:: PyPtt.BoardField.can_non_board_members_post 非看板成員是否可以發文 .. py:attribute:: PyPtt.BoardField.can_reply_post 是否可以回覆文章 .. py:attribute:: PyPtt.BoardField.self_del_post 是否可以自刪文章 .. py:attribute:: PyPtt.BoardField.can_comment_post 是否可以推文 .. py:attribute:: PyPtt.BoardField.can_boo_post 是否可以噓文 .. py:attribute:: PyPtt.BoardField.can_fast_push 是否可以快速推文 .. py:attribute:: PyPtt.BoardField.min_interval_between_comments 推文間隔時間 .. py:attribute:: PyPtt.BoardField.is_comment_record_ip 是否記錄推文 IP .. py:attribute:: PyPtt.BoardField.is_comment_aligned 推文是否對齊 .. py:attribute:: PyPtt.BoardField.can_moderators_del_illegal_content 板主是否可以刪除違規文字 .. py:attribute:: PyPtt.BoardField.does_tran_post_auto_recorded_and_require_post_permissions 是否自動記錄轉錄文章並需要發文權限 .. py:attribute:: PyPtt.BoardField.is_cool_mode 是否為冷板模式 .. py:attribute:: PyPtt.BoardField.is_require18 是否為 18 禁看板 .. py:attribute:: PyPtt.BoardField.require_login_time 發文需要登入次數 .. py:attribute:: PyPtt.BoardField.require_illegal_post 發文需要最低退文數量 .. py:attribute:: PyPtt.BoardField.post_kind_list 發文類別,例如 [公告] [問卦] 等 .. _post-field: PostField ----------- * 文章資料欄位。 .. py:attribute:: PyPtt.PostField.board 文章所在看板 .. py:attribute:: PyPtt.PostField.aid 文章 ID,例如:`#1Z69g2ts` .. py:attribute:: PyPtt.PostField.index 文章編號,例如:906 .. py:attribute:: PyPtt.PostField.author 文章作者 .. py:attribute:: PyPtt.PostField.date 文章日期 .. py:attribute:: PyPtt.PostField.title 文章標題 .. py:attribute:: PyPtt.PostField.content 文章內容 .. py:attribute:: PyPtt.PostField.money 文章稿酬,P 幣 .. py:attribute:: PyPtt.PostField.url 文章網址 .. py:attribute:: PyPtt.PostField.ip 文章 IP .. py:attribute:: PyPtt.PostField.comments 文章推文清單,詳見 :ref:`comment-field` .. py:attribute:: PyPtt.PostField.post_status 文章狀態,詳見 :ref:`post-status` .. py:attribute:: PyPtt.PostField.list_date 文章列表日期 .. py:attribute:: PyPtt.PostField.has_control_code 文章是否有控制碼 .. py:attribute:: PyPtt.PostField.pass_format_check 文章是否通過格式檢查 .. py:attribute:: PyPtt.PostField.location 文章 IP 位置 .. py:attribute:: PyPtt.PostField.push_number 文章推文數量 .. py:attribute:: PyPtt.PostField.is_lock 文章是否鎖定 .. py:attribute:: PyPtt.PostField.full_content 文章完整內容 .. py:attribute:: PyPtt.PostField.is_unconfirmed 文章是否為未確認文章 ================================================ FILE: make_doc.sh ================================================ make -C docs/ clean make -C docs/ html ================================================ FILE: requirements.txt ================================================ progressbar2 websockets uao requests==2.31.0 AutoStrEnum PyYAML ================================================ FILE: scripts/lang.py ================================================ import json import os import sys from collections import defaultdict import yaml sys.path.append(os.getcwd()) import PyPtt def add_lang(): new_words = [ (PyPtt.Language.MANDARIN, 'give_money', '給 _target0_ _target_ P 幣'), (PyPtt.Language.ENGLISH, 'give_money', 'give _target0_ _target_ P coins'), ] for lang, key, value in new_words: PyPtt.i18n.init(lang, cache=True) PyPtt.i18n._lang_data[key] = value with open(f'PyPtt/lang/{lang}.yaml', 'w', encoding='utf-8') as f: yaml.dump(PyPtt.i18n._lang_data, f, allow_unicode=True, default_flow_style=False) def check_lang(): import re # 搜尋 PyPtt 資料夾底下,所有用到 i18n 的字串 PyPtt.i18n.init(PyPtt.Language.MANDARIN, cache=True) # init count dict count_dict = {} for key, value in PyPtt.i18n._lang_data.items(): print('->', key, value) count_dict[key] = 0 # 1. 用 os.walk() 搜尋所有檔案 for dirpath, dirnames, filenames in os.walk('./PyPtt'): print(f'================= directory: {dirpath}') for file_name in filenames: if not file_name.endswith('.py'): continue if file_name == 'i18n.py': continue print(file_name) with open(f'{dirpath}/{file_name}', 'r', encoding='utf-8') as f: data = f.read() for match in re.finditer(r'i18n\.(\w+)', data): # print(match.group(0)) # print(match.group(1)) data_key = match.group(1) if data_key not in count_dict: print(f'Unknown key: {data_key}') else: count_dict[data_key] += 1 print('-----------------') print(json.dumps(count_dict, indent=4, ensure_ascii=False)) # collect the keys with 0 count zero_count_keys = [key for key, value in count_dict.items() if value == 0] for lang in PyPtt.i18n.locale_pool: PyPtt.i18n.init(lang, cache=True) for key in zero_count_keys: # remove the key from the lang data PyPtt.i18n._lang_data.pop(key, None) print(f'Removed key: {key} from {lang}.yaml') with open(f'PyPtt/lang/{lang}.yaml', 'w', encoding='utf-8') as f: yaml.dump(PyPtt.i18n._lang_data, f, allow_unicode=True, default_flow_style=False) if __name__ == '__main__': add_lang() # check_lang() pass ================================================ FILE: scripts/package_script.py ================================================ import os import subprocess import time def get_next_version(): is_merged = os.environ.get('GITHUB_EVENT_NAME') == 'pull_request' and os.environ.get( 'GITHUB_EVENT_ACTION') == 'closed' print('is_merged:', is_merged) # read the main version from __init__.py with open('PyPtt/__init__.py', 'r', encoding='utf-8') as f: data = f.read().strip() main_version = data.split('_main_version = ')[1].split('\n')[0].strip().strip('\'') print('main_version version:', main_version) version = None pypi_version = None for i in range(5): try: # Use wget to retrieve the PyPI version information subprocess.run(['wget', '-q', '-O', 'pypi_version.json', 'https://pypi.org/pypi/PyPtt/json'], check=True) with open('pypi_version.json', 'r', encoding='utf-8') as f: pypi_data = f.read() pypi_version = pypi_data.split('"version":')[1].split('"')[1] if pypi_version.startswith(main_version): min_pypi_version = pypi_version.split('.')[-1] # the next version version = f"{main_version}.{int(min_pypi_version) + 1}" else: version = f"{main_version}.0" break except subprocess.CalledProcessError: time.sleep(1) if version is None or pypi_version is None: raise ValueError('Can not get version from pypi') if not is_merged: commit_file = '/tmp/commit_hash.txt' if os.path.exists(commit_file): with open(commit_file, 'r', encoding='utf-8') as f: commit_hash = f.read().strip() else: max_hash_length = 5 try: commit_hash = subprocess.check_output(['git', 'rev-parse', '--long', 'HEAD']).decode('utf-8').strip() except subprocess.CalledProcessError: commit_hash = '0' * max_hash_length commit_hash = ''.join([x for x in list(commit_hash) if x.isdigit()]) if len(commit_hash) < max_hash_length: commit_hash = commit_hash + '0' * (max_hash_length - len(commit_hash)) commit_hash = commit_hash[:max_hash_length] with open(commit_file, 'w', encoding='utf-8') as f: f.write(commit_hash) version = f"{version}.dev{commit_hash}" if '__version__' in data: current_version = data.split('__version__ = ')[1].split('\n')[0].strip().strip('\'') data = data.replace(f"__version__ = '{current_version}'", f"__version__ = '{version}'") else: data += f'\n\n__version__ = \'{version}\'' with open('PyPtt/__init__.py', 'w', encoding='utf-8') as f: f.write(data) f.write('\n') return version ================================================ FILE: setup.py ================================================ import os import subprocess import time from setuptools import setup def version_automation_script(): is_merged = os.environ.get('GITHUB_EVENT_NAME') == 'pull_request' print('is_merged:', is_merged) # read the main version from __init__.py with open('PyPtt/__init__.py', 'r', encoding='utf-8') as f: data = f.read().strip() main_version = data.split('_main_version = ')[1].split('\n')[0].strip().strip('\'') print('main_version version:', main_version) version = None pypi_version = None for i in range(5): try: # Use wget to retrieve the PyPI version information subprocess.run(['wget', '-q', '-O', 'pypi_version.json', 'https://pypi.org/pypi/PyPtt/json'], check=True) with open('pypi_version.json', 'r', encoding='utf-8') as f: pypi_data = f.read() pypi_version = pypi_data.split('"version":')[1].split('"')[1] if pypi_version.startswith(main_version): min_pypi_version = pypi_version.split('.')[-1] # the next version version = f"{main_version}.{int(min_pypi_version) + 1}" else: version = f"{main_version}.0" break except subprocess.CalledProcessError: time.sleep(1) if version is None or pypi_version is None: raise ValueError('Can not get version from pypi') if not is_merged: commit_file = '/tmp/commit_hash.txt' if os.path.exists(commit_file): with open(commit_file, 'r', encoding='utf-8') as f: commit_hash = f.read().strip() else: max_hash_length = 5 try: commit_hash = subprocess.check_output(['git', 'rev-parse', '--long', 'HEAD']).decode('utf-8').strip() except subprocess.CalledProcessError: commit_hash = '0' * max_hash_length commit_hash = ''.join([x for x in list(commit_hash) if x.isdigit()]) if len(commit_hash) < max_hash_length: commit_hash = commit_hash + '0' * (max_hash_length - len(commit_hash)) commit_hash = commit_hash[:max_hash_length] with open(commit_file, 'w', encoding='utf-8') as f: f.write(commit_hash) version = f"{version}.dev{commit_hash}" if '__version__' in data: current_version = data.split('__version__ = ')[1].split('\n')[0].strip().strip('\'') data = data.replace(f"__version__ = '{current_version}'", f"__version__ = '{version}'") else: data += f'\n\n__version__ = \'{version}\'' with open('PyPtt/__init__.py', 'w', encoding='utf-8') as f: f.write(data) f.write('\n') return version version = version_automation_script() print('the next version:', version) setup( name='PyPtt', # Required version=version, # Required description='PyPtt\ngithub: https://github.com/PyPtt/PyPtt', # Required long_description=open('README.md', encoding="utf-8").read(), # Optional long_description_content_type='text/markdown', url='https://pyptt.cc/', # Optional author='CodingMan', # Optional author_email='pttcodingman@gmail.com', # Optional # https://pypi.org/classifiers/ classifiers=[ # Optional 'Development Status :: 5 - Production/Stable', 'Operating System :: OS Independent', 'Intended Audience :: Developers', 'Topic :: Communications :: BBS', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Internet', 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3 :: Only', 'Natural Language :: Chinese (Traditional)', 'Natural Language :: English', ], keywords=['PTT', 'crawler', 'bot', 'library', 'websockets'], # Optional python_requires='>=3.8', packages=['PyPtt'], install_requires=[ 'progressbar2', 'websockets', 'uao', 'requests', 'AutoStrEnum', 'PyYAML', ], package_data={ 'PyPtt': ['lang/*.yaml'], } ) ================================================ FILE: tests/change_pw.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from tests import util def test(ptt_bot: PyPtt.API): ptt_bot.change_pw(ptt_bot._ptt_pw) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/comment.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from tests import util def test(ptt_bot): if ptt_bot.host == PyPtt.HOST.PTT1: test_list = [ # comment the newest post ('Test', None), ] else: test_list = [ # comment the newest post ('Test', None), ] for board, post_id in test_list: if post_id is None: newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, board) for i in range(100): post_info = ptt_bot.get_post(board, index=newest_index - i) # if the post is not deleted, save the post if post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.EXISTS: break print('post_id', post_id) elif isinstance(post_id, int): post_info = ptt_bot.get_post(board, index=post_id, query=True) elif isinstance(post_id, str): post_info = ptt_bot.get_post(board, aid=post_id, query=True) print(post_info) # comment by index ptt_bot.comment( board=board, comment_type=PyPtt.CommentType.ARROW, content='comment by index', index=post_info['index'], ) # comment by aid ptt_bot.comment( board=board, comment_type=PyPtt.CommentType.ARROW, content='comment by aid', aid=post_info['aid'], ) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() # assert (result[0] == result[1]) if __name__ == '__main__': func() ================================================ FILE: tests/config.py ================================================ import os # PTT1_ID = os.environ['PTT1_ID'] PTT1_PW = os.environ['PTT1_PW'] PTT2_ID = os.environ['PTT2_ID'] PTT2_PW = os.environ['PTT2_PW'] ================================================ FILE: tests/exceptions.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt if __name__ == '__main__': try: raise PyPtt.NoPermission('test') except PyPtt.Error as e: print(e.__class__.__name__) ================================================ FILE: tests/get_board_info.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from PyPtt import log from tests import util def test(ptt_bot: PyPtt.API): test_board = [ 'SYSOP', ] for board in test_board: result = ptt_bot.get_board_info(board) log.logger.info('get board info result', result) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/get_board_list.py ================================================ import os import sys sys.path.append(os.getcwd()) import json import PyPtt from tests import util def test(ptt_bot: PyPtt.API): board_list = ptt_bot.get_all_boards() with open(f'tests/{ptt_bot.host}-board_list.json', 'w') as f: json.dump(board_list, f, indent=4, ensure_ascii=False) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/get_bottom_post_list.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from tests import util def test(ptt_bot: PyPtt.API): result = ptt_bot.get_bottom_post_list('Test') print(result) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/get_favourite_boards.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from tests import util def test(ptt_bot: PyPtt.API): result = ptt_bot.get_favourite_boards() for r in result: print(r) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/get_mail.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from PyPtt import log from tests import util def test(ptt_bot: PyPtt.API): result = [] for _ in range(3): mail_index = ptt_bot.get_newest_index(PyPtt.NewIndex.MAIL) mail_info = ptt_bot.get_mail(mail_index) log.logger.info('mail result', mail_info) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/get_newest_index.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from PyPtt import log from tests import util def test_board_index(ptt_bot: PyPtt.API): if ptt_bot.host == PyPtt.HOST.PTT1: test_list = [ ('Python', PyPtt.SearchType.KEYWORD, '[公告]'), ('ALLPOST', PyPtt.SearchType.KEYWORD, '(Wanted)'), ('Wanted', PyPtt.SearchType.KEYWORD, '(本文已被刪除)'), ('ALLPOST', PyPtt.SearchType.KEYWORD, '(Gossiping)'), ('Gossiping', PyPtt.SearchType.KEYWORD, '普悠瑪'), ('book', PyPtt.SearchType.KEYWORD, 'AWS'), ] else: test_list = [ ('PttSuggest', PyPtt.SearchType.KEYWORD, '[問題]'), # ('PttSuggest', PyPtt.SearchType.COMMENT, '10'), ] for board, search_type, search_condition in test_list: for _ in range(3): index = ptt_bot.get_newest_index( PyPtt.NewIndex.BOARD, board) log.logger.info(f'{board} newest index', index) index = ptt_bot.get_newest_index( PyPtt.NewIndex.BOARD, board=board, search_type=search_type, search_condition=search_condition) log.logger.info(f'{board} newest index with search', index) def test_mail_index(ptt_bot: PyPtt.API): for _ in range(3): index = ptt_bot.get_newest_index( PyPtt.NewIndex.MAIL) log.logger.info('mail newest index', index) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test_mail_index(ptt_bot) test_mail_index(ptt_bot) test_board_index(ptt_bot) test_board_index(ptt_bot) test_mail_index(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/get_post.py ================================================ import os import sys sys.path.append(os.getcwd()) import json import PyPtt from PyPtt import log from tests import util def test_no_condition(ptt_bot: PyPtt.API): result = [] if ptt_bot.host == PyPtt.HOST.PTT1: test_post_list = [ ('Python', 1), # ('NotExitBoard', 1), ('Python', '1TJH_XY0'), # ('Python', '1TJdL7L8'), # # 文章格式錯誤 # ('Stock', '1TVnEivO'), # # 文章格式錯誤 # ('movie', 457), # ('Gossiping', '1UDnXefr'), # ('joke', '1Tc6G9eQ'), # # 135193 # ('Test', 575), # # 待證文章 # ('Test', '1U3pLzi0'), # # 古早文章 # ('LAW', 1), # # 辦刪除文章 # ('Test', 347), # # comment number parse error # ('Ptt25sign', '1VppdKLW'), ] else: test_post_list = [ ('WhoAmI', 1), ] for board, index in test_post_list: if isinstance(index, int): post = ptt_bot.get_post( board, index=index) ptt_bot.get_post( board, index=index, query=True) else: post = ptt_bot.get_post( board, aid=index) ptt_bot.get_post( board, aid=index, query=True) result.append(post) # util.log.py.info('+==+' * 10) # util.log.py.info(post[PyPtt.PostField.content]) return result def get_post_with_condition(ptt_bot: PyPtt.API): def show_condition(test_board, search_type, condition): if search_type == PyPtt.SearchType.KEYWORD: type_str = '關鍵字' if search_type == PyPtt.SearchType.AUTHOR: type_str = '作者' if search_type == PyPtt.SearchType.COMMENT: type_str = '推文數' if search_type == PyPtt.SearchType.MARK: type_str = '標記' if search_type == PyPtt.SearchType.MONEY: type_str = '稿酬' log.logger.info(f'{test_board} 使用 {type_str} 搜尋 {condition}') if ptt_bot.config.host == PyPtt.HOST.PTT1: test_list = [ ('Python', PyPtt.SearchType.KEYWORD, '[公告]'), ('ALLPOST', PyPtt.SearchType.KEYWORD, '(Wanted)'), ('Wanted', PyPtt.SearchType.KEYWORD, '(本文已被刪除)'), ('ALLPOST', PyPtt.SearchType.KEYWORD, '(Gossiping)'), ('Gossiping', PyPtt.SearchType.KEYWORD, '普悠瑪'), ] else: test_list = [ ('PttSuggest', PyPtt.SearchType.KEYWORD, '[問題]'), ('PttSuggest', PyPtt.SearchType.COMMENT, '10'), ] result = [] test_range = 1 query = False for (board, search_type, condition) in test_list: show_condition(board, search_type, condition) index = ptt_bot.get_newest_index( PyPtt.NewIndex.BOARD, board, search_type=search_type, search_condition=condition) util.logger.info(f'{board} 最新文章編號 {index}') for i in range(test_range): post = ptt_bot.get_post( board, index=index - i, # PostIndex=611, search_type=search_type, search_condition=condition, query=query) # print(json.dumps(post, indent=4)) log.logger.info('列表日期', post.get('list_date')) log.logger.info('作者', post.get('author')) log.logger.info('標題', post.get('title')) if post.get('post_status') == PyPtt.PostStatus.EXISTS: pass # if not query: # util.log.py.info('內文', post.get('content')) elif post.get('post_status') == PyPtt.PostStatus.DELETED_BY_AUTHOR: log.logger.info('文章被作者刪除') elif post.get('post_status') == PyPtt.PostStatus.DELETED_BY_MODERATOR: log.logger.info('文章被版主刪除') log.logger.info('=' * 50) result.append(post) return result def test(ptt_bot: PyPtt.API): result = test_no_condition(ptt_bot) print(result) log.logger.info(json.dumps(result, indent=4, ensure_ascii=False)) # result = get_post_with_condition(ptt_bot) # util.log.py.info(json.dumps(result, ensure_ascii=False, indent=4)) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/get_time.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from PyPtt import log from tests import util def test(ptt_bot: PyPtt.API): result = [] for _ in range(10): result.append(ptt_bot.get_time()) # time.sleep(1) log.logger.info('get time result', result) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/get_user.py ================================================ import json import os import sys sys.path.append(os.getcwd()) import PyPtt from tests import util def test(ptt_bot: PyPtt.API): test_users = [ 'CodingMan', ] for test_user in test_users: user_info = ptt_bot.get_user(test_user) print(json.dumps(user_info, indent=4, ensure_ascii=False)) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/give_p.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from tests import util def test(ptt_bot: PyPtt.API): ptt_bot.give_money('janice001', 10) def func(): host_list = [ PyPtt.HOST.PTT1, # PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/i18n.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from PyPtt import i18n from PyPtt import log def test(): PyPtt.i18n.init(PyPtt.Language.ENGLISH) print(PyPtt.i18n.goodbye) logger = log.init(PyPtt.LogLevel.INFO, 'test') logger.info( i18n.replace(i18n.welcome, 'test version')) if __name__ == '__main__': test() ================================================ FILE: tests/init.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt def test(): print('=== default ===') PyPtt.API() print('=== 中文顯示 ===') PyPtt.API(language=PyPtt.Language.MANDARIN) print('=== 英文顯示 ===') PyPtt.API(language=PyPtt.Language.ENGLISH) print('=== log DEBUG ===') PyPtt.API(log_level=PyPtt.LogLevel.DEBUG) print('=== log INFO ===') PyPtt.API(log_level=PyPtt.LogLevel.INFO) print('=== log SILENT===') PyPtt.API(log_level=PyPtt.LogLevel.SILENT) print('=== set host with PTT ===') ptt_bot = PyPtt.API(host=PyPtt.HOST.PTT1) print(f'host result {ptt_bot.host}') print('=== set host with PTT2 ===') ptt_bot = PyPtt.API(host=PyPtt.HOST.PTT2) print(f'host result {ptt_bot.host}') print('=== set host with PTT and TELNET ===') try: PyPtt.API(host=PyPtt.HOST.PTT1, connect_mode=PyPtt.ConnectMode.TELNET) assert False except ValueError: print('通過') print('=== set host with PTT2 and TELNET ===') try: PyPtt.API(host=PyPtt.HOST.PTT2, connect_mode=PyPtt.ConnectMode.TELNET) assert False except ValueError: print('通過') try: print('=== 語言 99 ===') PyPtt.API(language=99) except TypeError: print('通過') except: print('沒通過') assert False print('=== 語言放字串 ===') try: PyPtt.API(language='PyPtt.i18n.language.ENGLISH') except TypeError: print('通過') except: print('沒通過') assert False print('complete') if __name__ == '__main__': test() # PyPtt.API() ================================================ FILE: tests/logger.py ================================================ import os import sys sys.path.append(os.getcwd()) from PyPtt import log def func(): logger = log.init(log.INFO) logger.info('1') logger.info('1', '2') logger.info('1', '2', '3') logger.debug('debug 1') logger.debug('1', '2') logger.debug('1', '2', '3') logger = log.init(log.DEBUG) logger.info('234') logger.info('1', '2') logger.info('1', '2', '3') logger.debug('debug 2') logger.debug('1', '2') logger.debug('1', '2', '3') if __name__ == '__main__': func() ================================================ FILE: tests/login_logout.py ================================================ import os import sys import time sys.path.append(os.getcwd()) import PyPtt from tests import util def test(ptt_bot: PyPtt.API): util.login(ptt_bot, kick=True) ptt_bot.logout() print('wait', end=' ') max_wait_time = 5 for sec in range(max_wait_time): print(max_wait_time - sec, end=' ') time.sleep(1) print() util.login(ptt_bot, kick=False) ptt_bot.logout() def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) test(ptt_bot) print('login logout test ok') if __name__ == '__main__': func() ================================================ FILE: tests/performance.py ================================================ import os import sys import time sys.path.append(os.getcwd()) import PyPtt from tests import util def test(ptt_bot): test_time = 500 print(f'效能測試 get_time {test_time} 次') start_time = time.time() for _ in range(test_time): ptt_time = ptt_bot.get_time() assert ptt_time is not None end_time = time.time() print( F'Performance Test get_time {end_time - start_time} s') print('Performance Test finish') def func(): ptt_bot_list = [ PyPtt.API()] for ptt_bot in ptt_bot_list: util.login(ptt_bot) test(ptt_bot) ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/post.py ================================================ import os import sys sys.path.append(os.getcwd()) import time import PyPtt from PyPtt import PostField from tests import util from PyPtt import log def test(ptt_bot: PyPtt.API): content = ''' 此為 PyPtt 貼文測試內容,如有打擾請告知。 官方網站: https://pyptt.cc 測試標記 781d16268c9f25a39142a17ff063ac029b1466ca14cb34f5d88fe8aadfeee053 ''' temp = '' for i in range(100): content = f'{content}\n={i}=' temp = f'{temp}\n={i}=' check_ = [ '781d16268c9f25a39142a17ff063ac029b1466ca14cb34f5d88fe8aadfeee053', temp ] check_range = 3 for _ in range(check_range): ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content=content, sign_file=0) time.sleep(1) newest_index = ptt_bot.get_newest_index(index_type=PyPtt.NewIndex.BOARD, board='Test') # find post what we post post_list = [] for i in range(10): post = ptt_bot.get_post(board='Test', index=newest_index - i) if post[PostField.post_status] != PyPtt.PostStatus.EXISTS: print(f'Post {newest_index - i} not exists') continue post_author = post[PostField.author] post_author = post_author.split(' ')[0] if post_author != ptt_bot.ptt_id: print(f'Post {newest_index - i} author not match', post_author) continue post_list.append(newest_index - i) if len(post_list) == check_range: break comment_check = [] for index in post_list: for i in range(5): comment_check.append(f'={i}=') ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.ARROW, content=f'={i}=', index=index) comment_check = list(set(comment_check)) time.sleep(1) for i, index in enumerate(post_list): log.logger.info('test', i) post = ptt_bot.get_post(board='Test', index=index) if post[PostField.post_status] != PyPtt.PostStatus.EXISTS: log.logger.info('fail') print(f'Post {index} not exists') break post_author = post[PostField.author] post_author = post_author.split(' ')[0] if post_author != ptt_bot.ptt_id: log.logger.info('fail') print(f'Post {index} author not match', post_author) break check = True for c in check_: if c not in post[PostField.content]: check = False break if not check: log.logger.info('fail') print(f'Post {index} content not match') break cur_comment_check = set() for comment in post[PostField.comments]: if comment[PyPtt.CommentField.content] in comment_check: cur_comment_check.add(comment[PyPtt.CommentField.content]) else: log.logger.info('comment', comment[PyPtt.CommentField.content]) if len(cur_comment_check) != len(comment_check): log.logger.info('fail') print(f'Post {index} comment not match') break log.logger.info('pass') # for index in post_list: # ptt_bot.del_post(board='Test', index=index) util.del_all_post(ptt_bot) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) util.login(ptt_bot) test(ptt_bot) ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/reply.py ================================================ import os import sys import time sys.path.append(os.getcwd()) import PyPtt from PyPtt import log from PyPtt import PostField from tests import util current_id = None def test(ptt_bot: PyPtt.API): ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content='測試內文', sign_file=0) time.sleep(1) newest_index = ptt_bot.get_newest_index(index_type=PyPtt.NewIndex.BOARD, board='Test') for i in range(5): cur_post = ptt_bot.get_post(board='Test', index=newest_index - i) if cur_post[PostField.post_status] != PyPtt.PostStatus.EXISTS: continue cur_author = cur_post[PostField.author] cur_author = cur_author.split(' ')[0] if cur_author.lower() != ptt_bot.ptt_id.lower(): continue ptt_bot.reply_post( reply_to=PyPtt.ReplyTo.BOARD, board='Test', index=newest_index - i, content='PyPtt 程式回覆測試') break newest_index += 1 time.sleep(1) posts = [] # 在十篇範圍內找尋我們的文章 for i in range(10): cur_post = ptt_bot.get_post(board='Test', index=newest_index - i) if cur_post[PostField.post_status] != PyPtt.PostStatus.EXISTS: continue cur_author = cur_post[PostField.author] cur_author = cur_author.split(' ')[0] if cur_author.lower() != ptt_bot.ptt_id.lower(): continue posts.append(cur_post[PostField.aid]) log.logger.info('test') if len(posts) < 2: log.logger.info('len(posts) < 2, fail') return check = [ '[測試] PyPtt 程式貼文測試', 'Re: [測試] PyPtt 程式貼文測試' ] check_result = True for aid in posts: post = ptt_bot.get_post(board='Test', aid=aid) if post[PostField.post_status] != PyPtt.PostStatus.EXISTS: log.logger.info('post[PostField.post_status] != PyPtt.PostStatus.EXISTS, fail') check_result = False break if post[PostField.title] not in check: log.logger.info('post[PostField.title] not in check, fail') check_result = False break check.remove(post[PostField.title]) if check_result: log.logger.info('pass') else: log.logger.info('fail') util.del_all_post(ptt_bot) def func(): global current_id host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/search_user.py ================================================ import os import sys sys.path.append(os.getcwd()) import PyPtt from PyPtt import log from tests import util def test(ptt_bot: PyPtt.API): test_users = [ 'Coding', ] for test_user in test_users: result = ptt_bot.search_user(test_user) log.logger.info('result', result) def func(): host_list = [ PyPtt.HOST.PTT1, PyPtt.HOST.PTT2 ] for host in host_list: ptt_bot = PyPtt.API( host=host, # log_level=PyPtt.LogLevel.DEBUG, ) try: util.login(ptt_bot) test(ptt_bot) finally: ptt_bot.logout() if __name__ == '__main__': func() ================================================ FILE: tests/service.py ================================================ import os import sys import threading sys.path.append(os.getcwd()) import PyPtt from PyPtt import Service from tests import config def api_test(thread_id, service): result = service.call('get_time') print(f'thread id {thread_id}', 'get_time', result) result = service.call('get_aid_from_url', {'url': 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html'}) print(f'thread id {thread_id}', 'get_aid_from_url', result) result = service.call('get_newest_index', {'index_type': PyPtt.NewIndex.BOARD, 'board': 'Python'}) print(f'thread id {thread_id}', 'get_newest_index', result) def test(): pyptt_init_config = { # 'language': PyPtt.Language.ENGLISH, } service = Service(pyptt_init_config) try: service.call('login', {'ptt_id': config.PTT1_ID, 'ptt_pw': config.PTT1_PW}) pool = [] for i in range(10): t = threading.Thread(target=api_test, args=(i, service)) t.start() pool.append(t) for t in pool: t.join() service.call('logout') finally: service.close() if __name__ == '__main__': test() # pass ================================================ FILE: tests/util.py ================================================ import json import PyPtt from PyPtt import PostField from PyPtt import log from . import config def log_to_file(msg: str): with open('single_log.txt', 'a', encoding='utf8') as f: f.write(f'{msg}\n') def get_id_pw(password_file): try: with open(password_file) as AccountFile: account = json.load(AccountFile) ptt_id = account['id'] password = account['pw'] except FileNotFoundError: print(f'Please write PTT ID and Password in {password_file}') print('{"id":"your ptt id", "pw":"your ptt pw"}') assert False return ptt_id, password def login(ptt_bot: PyPtt.API, kick: bool = True): if ptt_bot.host == PyPtt.HOST.PTT1: ptt_id, ptt_pw = config.PTT1_ID, config.PTT1_PW else: ptt_id, ptt_pw = config.PTT2_ID, config.PTT2_PW for _ in range(3): try: ptt_bot.login(ptt_id=ptt_id, ptt_pw=ptt_pw, kick_other_session=kick) break except PyPtt.LoginError: log.logger.info('登入失敗') assert False except PyPtt.WrongIDorPassword: log.logger.info('帳號密碼錯誤') assert False except PyPtt.LoginTooOften: log.logger.info('請稍等一下再登入') assert False if not ptt_bot.is_registered_user: log.logger.info('未註冊使用者') if ptt_bot.process_picks != 0: log.logger.info(f'註冊單處理順位 {ptt_bot.process_picks}') def show_data(data, key: str = None): if isinstance(data, dict): log.logger.info(f'{key}: {data[key]}') def del_all_post(ptt_bot: PyPtt.API): newest_index = ptt_bot.get_newest_index(index_type=PyPtt.NewIndex.BOARD, board='Test') for i in range(30): try: ptt_bot.del_post(board='Test', index=newest_index - i) except: pass ================================================ FILE: upload.sh ================================================ #!/bin/sh echo PyPtt uploader v 1.0.2 rm -r dist build python3 setup.py sdist bdist_wheel --universal case $1 in release) echo upload to pypi python3 -m twine upload dist/* ;; test) echo upload to testpypi python3 -m twine upload --repository testpypi dist/* ;; *) echo "unknown command [$@]" ;; esac echo Upload finish