[
  {
    "path": ".github/workflows/deploy.yml",
    "content": "# This workflow will install Python dependencies, run tests and lint with a variety of Python versions\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions\n\nname: deploy\n\n# run on merge to master or manual trigger\non:\n  pull_request:\n    types: [ closed ]\n    branches:\n      - master\n    paths:\n      - 'PyPtt/*.py'\n      - 'setup.py'\n  workflow_dispatch:\n\njobs:\n  deploy:\n    name: Deploy to PyPI and Docker\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.10'\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install build\n      - name: Build package\n        run: |\n          python -m build\n      - name: Publish package to TestPyPI\n        if: github.event_name == 'workflow_dispatch' && github.event.pull_request.merged == false\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          password: ${{ secrets.TEST_PYPI_API_TOKEN }}\n          repository_url: https://test.pypi.org/legacy/\n      - name: Publish package\n        if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          password: ${{ secrets.PYPI_API_TOKEN }}\n      - name: Trigger Docker build\n        if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true\n        uses: InformaticsMatters/trigger-ci-action@1.0.1\n        with:\n          ci-owner: PyPtt\n          ci-repository: PyPtt_image\n          ci-user: PttCodingMan\n          ci-ref: refs/heads/main\n          ci-user-token: ${{ secrets.ACCESS_TOKEN }}\n          ci-name: build PyPtt image\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "\nname: docs\n\n# run on merge to master or manual trigger\non:\n  pull_request:\n    types: [ closed ]\n    paths:\n      - 'docs/**/*'\n      - 'PyPtt/*.py'\n  workflow_dispatch:\n\njobs:\n  build:\n    if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true\n    name: Build doc and Deploy\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/setup-python@v5\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0 # otherwise, you will failed to push refs to dest repo\n    - name: Build and Commit\n      uses: sphinx-notes/pages@v2\n      with:\n        requirements_path: docs/requirements.txt\n    - name: Push changes\n      uses: ad-m/github-push-action@master\n      with:\n        github_token: ${{ secrets.GITHUB_TOKEN }}\n        branch: gh-pages\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "# This workflow will install Python dependencies, run tests and lint with a single version of Python\n# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions\n\nname: test\n\n# run on every PR creation, or manual trigger\n# run on every push to non-master branch\non:\n  pull_request:\n    types:\n      - opened\n  push:\n    branches-ignore:\n      - 'master'\n  workflow_dispatch:\n  \nenv:\n  DEP_PATH: requirements.txt\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [ \"3.8\", \"3.9\", \"3.10\", \"3.11\", \"3.12\" ]\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install flake8 pytest\n          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi\n      - name: Lint with flake8\n        run: |\n          # stop the build if there are Python syntax errors or undefined names\n          flake8 PyPtt/ --count --select=E9,F63,F7,F82 --show-source --statistics\n          # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide\n          flake8 PyPtt/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics\n      - name: Test\n        run: |\n          python tests/init.py\n  scan:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out master\n        uses: actions/checkout@v4\n\n      - name: detect-secrets\n        uses: reviewdog/action-detect-secrets@master\n        with:\n          reporter: github-pr-review\n\n      - name: Security vulnerabilities scan\n        uses: aufdenpunkt/python-safety-check@master\n        with:\n          scan_requirements_file_only: true\n    \n"
  },
  {
    "path": ".gitignore",
    "content": "__pycache__/\nbuild/\ndist/\n/CrawlBoardResult.txt\n/.pypirc\nPTTLibrary.egg-info/\n*Out.txt\n/Big5Data.txt\nPTTLibrary-*/\n/PTTLibrary/i18n.txt\n/Test.txt\nAccount*.txt\n*.spec\n/LogHandler.txt\n.vscode\n.idea/\nvenv/\n/log.txt\nPyPtt.egg-info/\n/test_account*.txt\n/test_result.txt\n*.json\ntests/ptt.sh\ndocs/_build/\n.DS_Store\ntest*.py\nptt.sh\n"
  },
  {
    "path": "GourceScript.bat",
    "content": "@echo off\ncls\n\ngource --seconds-per-day 0.05 --title \"PTT Library\""
  },
  {
    "path": "LICENSE",
    "content": "                   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n\n  This version of the GNU Lesser General Public License incorporates\nthe terms and conditions of version 3 of the GNU General Public\nLicense, supplemented by the additional permissions listed below.\n\n  0. Additional Definitions.\n\n  As used herein, \"this License\" refers to version 3 of the GNU Lesser\nGeneral Public License, and the \"GNU GPL\" refers to version 3 of the GNU\nGeneral Public License.\n\n  \"The Library\" refers to a covered work governed by this License,\nother than an Application or a Combined Work as defined below.\n\n  An \"Application\" is any work that makes use of an interface provided\nby the Library, but which is not otherwise based on the Library.\nDefining a subclass of a class defined by the Library is deemed a mode\nof using an interface provided by the Library.\n\n  A \"Combined Work\" is a work produced by combining or linking an\nApplication with the Library.  The particular version of the Library\nwith which the Combined Work was made is also called the \"Linked\nVersion\".\n\n  The \"Minimal Corresponding Source\" for a Combined Work means the\nCorresponding Source for the Combined Work, excluding any source code\nfor portions of the Combined Work that, considered in isolation, are\nbased on the Application, and not on the Linked Version.\n\n  The \"Corresponding Application Code\" for a Combined Work means the\nobject code and/or source code for the Application, including any data\nand utility programs needed for reproducing the Combined Work from the\nApplication, but excluding the System Libraries of the Combined Work.\n\n  1. Exception to Section 3 of the GNU GPL.\n\n  You may convey a covered work under sections 3 and 4 of this License\nwithout being bound by section 3 of the GNU GPL.\n\n  2. Conveying Modified Versions.\n\n  If you modify a copy of the Library, and, in your modifications, a\nfacility refers to a function or data to be supplied by an Application\nthat uses the facility (other than as an argument passed when the\nfacility is invoked), then you may convey a copy of the modified\nversion:\n\n   a) under this License, provided that you make a good faith effort to\n   ensure that, in the event an Application does not supply the\n   function or data, the facility still operates, and performs\n   whatever part of its purpose remains meaningful, or\n\n   b) under the GNU GPL, with none of the additional permissions of\n   this License applicable to that copy.\n\n  3. Object Code Incorporating Material from Library Header Files.\n\n  The object code form of an Application may incorporate material from\na header file that is part of the Library.  You may convey such object\ncode under terms of your choice, provided that, if the incorporated\nmaterial is not limited to numerical parameters, data structure\nlayouts and accessors, or small macros, inline functions and templates\n(ten or fewer lines in length), you do both of the following:\n\n   a) Give prominent notice with each copy of the object code that the\n   Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the object code with a copy of the GNU GPL and this license\n   document.\n\n  4. Combined Works.\n\n  You may convey a Combined Work under terms of your choice that,\ntaken together, effectively do not restrict modification of the\nportions of the Library contained in the Combined Work and reverse\nengineering for debugging such modifications, if you also do each of\nthe following:\n\n   a) Give prominent notice with each copy of the Combined Work that\n   the Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the Combined Work with a copy of the GNU GPL and this license\n   document.\n\n   c) For a Combined Work that displays copyright notices during\n   execution, include the copyright notice for the Library among\n   these notices, as well as a reference directing the user to the\n   copies of the GNU GPL and this license document.\n\n   d) Do one of the following:\n\n       0) Convey the Minimal Corresponding Source under the terms of this\n       License, and the Corresponding Application Code in a form\n       suitable for, and under terms that permit, the user to\n       recombine or relink the Application with a modified version of\n       the Linked Version to produce a modified Combined Work, in the\n       manner specified by section 6 of the GNU GPL for conveying\n       Corresponding Source.\n\n       1) Use a suitable shared library mechanism for linking with the\n       Library.  A suitable mechanism is one that (a) uses at run time\n       a copy of the Library already present on the user's computer\n       system, and (b) will operate properly with a modified version\n       of the Library that is interface-compatible with the Linked\n       Version.\n\n   e) Provide Installation Information, but only if you would otherwise\n   be required to provide such information under section 6 of the\n   GNU GPL, and only to the extent that such information is\n   necessary to install and execute a modified version of the\n   Combined Work produced by recombining or relinking the\n   Application with a modified version of the Linked Version. (If\n   you use option 4d0, the Installation Information must accompany\n   the Minimal Corresponding Source and Corresponding Application\n   Code. If you use option 4d1, you must provide the Installation\n   Information in the manner specified by section 6 of the GNU GPL\n   for conveying Corresponding Source.)\n\n  5. Combined Libraries.\n\n  You may place library facilities that are a work based on the\nLibrary side by side in a single library together with other library\nfacilities that are not Applications and are not covered by this\nLicense, and convey such a combined library under terms of your\nchoice, if you do both of the following:\n\n   a) Accompany the combined library with a copy of the same work based\n   on the Library, uncombined with any other library facilities,\n   conveyed under the terms of this License.\n\n   b) Give prominent notice with the combined library that part of it\n   is a work based on the Library, and explaining where to find the\n   accompanying uncombined form of the same work.\n\n  6. Revised Versions of the GNU Lesser General Public License.\n\n  The Free Software Foundation may publish revised and/or new versions\nof the GNU Lesser General Public License from time to time. Such new\nversions will be similar in spirit to the present version, but may\ndiffer in detail to address new problems or concerns.\n\n  Each version is given a distinguishing version number. If the\nLibrary as you received it specifies that a certain numbered version\nof the GNU Lesser General Public License \"or any later version\"\napplies to it, you have the option of following the terms and\nconditions either of that published version or of any later version\npublished by the Free Software Foundation. If the Library as you\nreceived it does not specify a version number of the GNU Lesser\nGeneral Public License, you may choose any version of the GNU Lesser\nGeneral Public License ever published by the Free Software Foundation.\n\n  If the Library as you received it specifies that a proxy can decide\nwhether future versions of the GNU Lesser General Public License shall\napply, that proxy's public statement of acceptance of any version is\npermanent authorization for you to choose that version for the\nLibrary.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE\ninclude README.md\n\n# Include the data files\nrecursive-include PyPtt *"
  },
  {
    "path": "PyPtt/PTT.py",
    "content": "﻿from __future__ import annotations\n\nimport functools\nimport threading\nfrom typing import Dict, Tuple, Callable, List, Optional, Any\n\nfrom . import __version__\nfrom . import _api_bucket\nfrom . import _api_change_pw\nfrom . import _api_comment\nfrom . import _api_del_post\nfrom . import _api_get_board_info\nfrom . import _api_get_board_list\nfrom . import _api_get_bottom_post_list\nfrom . import _api_get_favourite_board\nfrom . import _api_get_newest_index\nfrom . import _api_get_post\nfrom . import _api_get_time\nfrom . import _api_get_user\nfrom . import _api_give_money\nfrom . import _api_loginout\nfrom . import _api_mail\nfrom . import _api_mark_post\nfrom . import _api_post\nfrom . import _api_reply_post\nfrom . import _api_search_user\nfrom . import _api_set_board_title\nfrom . import check_value\nfrom . import config\nfrom . import connect_core\nfrom . import data_type\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\n\n\nclass API:\n    def __init__(self, **kwargs):\n        \"\"\"\n\n        初始化 PyPtt。\n\n        Args:\n            language (:ref:`language`): PyPtt 顯示訊息的語言。預設為 **MANDARIN**。\n            log_level (LogLevel_): PyPtt 顯示訊息的等級。預設為 **INFO**。\n            screen_timeout (int): 經過 screen_timeout 秒之後， PyPtt 將會判定無法判斷目前畫面的狀況。預設為 **3 秒**。\n            screen_long_timeout (int): 經過 screen_long_timeout 秒之後，PyPtt 將會判定無法判斷目前畫面的狀況，這會用在較長的等待時間，例如踢掉其他連線等等。預設為 **10 秒**。\n            screen_post_timeout (int): 經過 screen_post_timeout 秒之後，PyPtt 將會判定無法判斷目前畫面的狀況，這會用在較長的等待時間，例如發佈文章等等。預設為 **60 秒**。\n            connect_mode (:ref:`connect-mode`): PyPtt 連線的模式。預設為 **WEBSOCKETS**。\n            logger_callback (Callable): PyPtt 顯示訊息的 callback。預設為 None。\n            port (int): PyPtt 連線的 port。預設為 **23**。\n            host (:ref:`host`): PyPtt 連線的 PTT 伺服器。預設為 **PTT1**。\n            check_update (bool): 是否檢查 PyPtt 的更新。預設為 **True**。\n\n        Returns:\n            None\n\n        範例::\n\n            import PyPtt\n            ptt_bot = PyPtt.API()\n\n        參考: :ref:`language`、LogLevel_、:ref:`connect-mode`、:ref:`host`\n\n        .. _LogLevel: https://github.com/PttCodingMan/SingleLog/blob/d7c19a1b848dfb1c9df8201f13def9a31afd035c/SingleLog/SingleLog.py#L22\n\n        英文顯示範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API(\n                language=PyPtt.Language.ENGLISH)\n\n        除錯範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API(\n                log_level=PyPtt.LogLevel.DEBUG)\n\n\n        \"\"\"\n\n        log_level = kwargs.get('log_level', log.INFO)\n        if not isinstance(log_level, log.LogLv):\n            raise TypeError('[PyPtt] log_level must be log.Level')\n\n        logger_callback = kwargs.get('logger_callback', None)\n        log.init(log_level, logger_callback=logger_callback)\n\n        language = kwargs.get('language', data_type.Language.MANDARIN)\n        if not isinstance(language, str):\n            raise TypeError('[PyPtt] language must be PyPtt.Language')\n        if language not in i18n.locale_pool:\n            raise TypeError('[PyPtt] language must be PyPtt.Language')\n\n        self.config = config.Config()\n        self.config.log_level = log_level\n\n        self.config.language = language\n\n        print('language', self.config.language)\n        i18n.init(self.config.language)\n\n        self.is_mailbox_full: bool = False\n        self.is_registered_user: bool = False\n        self.process_picks: int = 0\n\n        self.ptt_id: str = ''\n        self._ptt_pw: str = ''\n        self._is_login: bool = False\n\n        host = kwargs.get('host', data_type.HOST.PTT1)\n        screen_timeout = kwargs.get('screen_timeout', 3.0)\n        screen_long_timeout = kwargs.get('screen_long_timeout', 10.0)\n        screen_post_timeout = kwargs.get('screen_post_timeout', 60.0)\n\n        check_value.check_type(host, (data_type.HOST, str), 'host')\n        check_value.check_type(screen_timeout, float, 'screen_timeout')\n        check_value.check_type(screen_long_timeout, float, 'screen_long_timeout')\n        check_value.check_type(screen_post_timeout, float, 'screen_post_timeout')\n\n        if screen_timeout != 0:\n            self.config.screen_timeout = screen_timeout\n        if screen_long_timeout != 0:\n            self.config.screen_long_timeout = screen_long_timeout\n        if screen_post_timeout != 0:\n            self.config.screen_post_timeout = screen_post_timeout\n\n        self.config.host = host\n        self.host = host\n\n        port = kwargs.get('port', 23)\n\n        check_value.check_type(port, int, 'port')\n        check_value.check_range(port, 1, 65535 - 1, 'port')\n        self.config.port = port\n\n        connect_mode = kwargs.get('connect_mode', data_type.ConnectMode.WEBSOCKETS)\n\n        check_value.check_type(connect_mode, data_type.ConnectMode, 'connect_mode')\n        if host in [data_type.HOST.PTT1, data_type.HOST.PTT2] and connect_mode is data_type.ConnectMode.TELNET:\n            raise ValueError('[PyPtt] TELNET is not available on PTT1 and PTT2')\n        self.config.connect_mode = connect_mode\n\n        self.connect_core = connect_core.API(self.config)\n        self._exist_board_list = []\n        self._moderators = dict()\n        self._thread_id = threading.get_ident()\n        self._goto_board_list = []\n        self._board_info_list = dict()\n        self._newest_index_data = data_type.TimedDict(timeout=2)\n\n        log.logger.debug('thread_id', self._thread_id)\n\n        log.logger.info(\n            i18n.replace(i18n.welcome, __version__))\n\n        log.logger.info('PyPtt', i18n.initialization)\n\n        if self.config.connect_mode == data_type.ConnectMode.TELNET:\n            log.logger.info(i18n.set_connect_mode, '...', i18n.connect_mode_TELNET)\n        elif self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:\n            log.logger.info(i18n.set_connect_mode, '...', i18n.connect_mode_WEBSOCKET)\n\n        if self.config.language == data_type.Language.MANDARIN:\n            log.logger.info(i18n.set_up_lang_module, '...', i18n.mandarin_module)\n        elif self.config.language == data_type.Language.ENGLISH:\n            log.logger.info(i18n.set_up_lang_module, '...', i18n.english_module)\n\n        if self.config.host == data_type.HOST.PTT1:\n            log.logger.info(i18n.set_connect_host, '...', i18n.PTT)\n        elif self.config.host == data_type.HOST.PTT2:\n            log.logger.info(i18n.set_connect_host, '...', i18n.PTT2)\n        elif self.config.host == data_type.HOST.LOCALHOST:\n            log.logger.info(i18n.set_connect_host, '...', i18n.localhost)\n        else:\n            log.logger.info(i18n.set_connect_host, '...', self.config.host)\n\n        log.logger.info('PyPtt', i18n.initialization, '...', i18n.done)\n\n        check_update = kwargs.get('check_update', True)\n        check_value.check_type(check_update, bool, 'check_update')\n\n        if check_update:\n            version_compare, remote_version = lib_util.sync_version()\n\n            if version_compare is data_type.Compare.SMALLER:\n                log.logger.info(i18n.current_version, __version__)\n                log.logger.info(i18n.new_version, remote_version)\n            elif version_compare is data_type.Compare.BIGGER:\n                log.logger.info(i18n.development_version, __version__)\n            else:\n                log.logger.info(i18n.latest_version, __version__)\n        else:\n            log.logger.info(i18n.current_version, __version__)\n\n    def __del__(self):\n        if log.logger:\n            log.logger.debug(i18n.goodbye)\n\n    def login(self, ptt_id: str, ptt_pw: str, kick_other_session: bool = False) -> None:\n\n        \"\"\"\n        登入 PTT。\n\n        Args:\n            ptt_id (str): PTT ID。\n            ptt_pw (str): PTT 密碼。\n            kick_other_session (bool): 是否踢掉其他登入的 session。預設為 False。\n\n        Returns:\n            None\n\n        Raises:\n            LoginError: 登入失敗。\n            WrongIDorPassword: 帳號或密碼錯誤。\n            OnlySecureConnection: 只能使用安全連線。\n            ResetYourContactEmail: 請先至信箱設定連絡信箱。\n\n        範例::\n\n            import PyPtt\n            ptt_bot = PyPtt.API()\n\n            try:\n                ptt_bot.login(\n                    ptt_id='ptt_id', ptt_pw='ptt_pw', kick_other_session=True)\n            except PyPtt.LoginError:\n                print('登入失敗')\n            except PyPtt.WrongIDorPassword:\n                print('帳號密碼錯誤')\n            except PyPtt.OnlySecureConnection:\n                print('只能使用安全連線')\n            except PyPtt.ResetYourContactEmail:\n                print('請先至信箱設定連絡信箱')\n\n        \"\"\"\n\n        _api_loginout.login(self, ptt_id, ptt_pw, kick_other_session)\n\n    def logout(self) -> None:\n        \"\"\"\n        登出 PTT。\n\n        Returns:\n            None\n\n        範例::\n\n            import PyPtt\n            ptt_bot = PyPtt.API()\n\n            try:\n                # .. login ..\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        \"\"\"\n\n        _api_loginout.logout(self)\n\n    def get_time(self) -> str:\n\n        \"\"\"\n        取得 PTT 系統時間。\n\n        Returns:\n            None\n\n        範例::\n\n            import PyPtt\n            ptt_bot = PyPtt.API()\n\n            try:\n                # .. login ..\n                time = ptt_bot.get_time()\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n        \"\"\"\n\n        return _api_get_time.get_time(self)\n\n    def get_post(self, board: str, aid: Optional[str] = None, index: Optional[int] = None,\n                 search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None,\n                 search_list: Optional[List[tuple]] = None, query: bool = False) -> Dict:\n        \"\"\"\n        取得文章。\n\n        Args:\n            board (str): 看板名稱。\n            aid (str): 文章編號。\n            index: 文章編號。\n            search_list (List[str]): 搜尋清單。\n            query (bool): 是否為查詢模式。\n\n        Returns:\n            Dict，文章內容。詳見 :ref:`post-field`\n\n        Raises:\n            RequireLogin: 需要登入。\n            NoSuchBoard: 看板不存在。\n\n        使用 AID 範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                post_info = ptt_bot.get_post('Python', aid='1TJH_XY0')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        使用 index 範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                post_info = ptt_bot.get_post('Python', index=1)\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        使用搜尋範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                post_info = ptt_bot.get_post(\n                    'Python',\n                    index=1,\n                    search_list=[(PyPtt.SearchType.KEYWORD, 'PyPtt')]\n                )\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        | 更多範例參考 :ref:`取得文章 <check_post_status>`\n        | 參考 :ref:`取得最新文章編號 <api-get-newest-index>`\n        \"\"\"\n        return _api_get_post.get_post(\n            self, board, aid=aid, index=index, search_type=search_type, search_condition=search_condition,\n            search_list=search_list, query=query)\n\n    def get_newest_index(self, index_type: data_type.NewIndex, board: Optional[str] = None,\n                         search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None,\n                         search_list: Optional[List[Tuple[Any | str]]] = None, ) -> int:\n        \"\"\"\n        取得最新文章或信箱編號。\n\n        Args:\n            index_type (:ref:`new-index`): 編號類型。\n            board (str): 看板名稱。\n            search_list (List[str]): 搜尋清單。\n\n        Returns:\n            int，最新文章或信箱編號。\n\n        Raises:\n            RequireLogin: 需要登入。\n            NoSuchBoard: 看板不存在。\n\n        取得最新看板編號::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n\n            # get newest index of board\n            try:\n                # .. login ..\n                newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, 'Python')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n\n        取得最新文章編號使用搜尋::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n\n            search_list = [(PyPtt.SearchType.KEYWORD, 'PyPtt')]\n\n            try:\n                # .. login ..\n                newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, 'Python', search_list=search_list)\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        取得最新信箱編號::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n\n            # get newest index of mail\n            try:\n                # .. login ..\n                newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.MAIL)\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        參考 :ref:`搜尋編號種類 <new-index>`、:ref:`取得文章 <api-get-post>`\n        \"\"\"\n\n        return _api_get_newest_index.get_newest_index(\n            self, index_type, board, search_type, search_condition, search_list)\n\n    def post(self, board: str, title_index: int, title: str, content: str, sign_file: [str | int] = 0) -> None:\n        \"\"\"\n        發文。\n\n        Args:\n            board (str): 看板名稱。\n            title_index (int): 文章標題編號。\n            title (str): 文章標題。\n            content (str): 文章內容。\n            sign_file  (str | int): 編號或隨機簽名檔 (x)，預設為 0 (不選)。\n\n        Returns:\n            None\n\n        Raises:\n            UnregisteredUser: 未註冊使用者。\n            RequireLogin: 需要登入。\n            NoSuchBoard: 看板不存在。\n            NoPermission: 沒有發佈權限。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content='測試內容', sign_file=0)\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        \"\"\"\n\n        _api_post.post(self, board, title, content, title_index, sign_file)\n\n    def comment(self, board: str, comment_type: data_type.CommentType, content: str, aid: Optional[str] = None,\n                index: int = 0) -> None:\n        \"\"\"\n        推文。\n\n        Args:\n            board (str): 看板名稱。\n            comment_type (:ref:`comment-type`): 推文類型。\n            content (str): 推文內容。\n            aid (str): 文章編號。\n            index (int): 文章編號。\n\n        Returns:\n            None\n\n        Raises:\n            UnregisteredUser: 未註冊使用者。\n            RequireLogin: 需要登入。\n            NoSuchBoard: 看板不存在。\n            NoSuchPost: 文章不存在。\n            NoPermission: 沒有推文權限。\n            NoFastComment: 推文間隔太短。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.PUSH, content='Comment by index', index=123)\n                ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.PUSH, content='Comment by index', aid='17MrayxF')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        參考 :ref:`推文類型 <comment-type>`、:ref:`取得最新文章編號 <api-get-newest-index>`\n        \"\"\"\n\n        _api_comment.comment(self, board, comment_type, content, aid, index)\n\n    def get_user(self, user_id: str) -> Dict:\n\n        \"\"\"\n        取得使用者資訊。\n\n        Args:\n            user_id (str): 使用者 ID。\n\n        Returns:\n            Dict，使用者資訊。詳見 :ref:`使用者資料欄位 <user-field>`\n\n        Raises:\n            RequireLogin: 需要登入。\n            NoSuchUser: 使用者不存在。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                user_info = ptt_bot.get_user('CodingMan')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        參考 :ref:`使用者資料欄位 <user-field>`\n\n        \"\"\"\n\n        return _api_get_user.get_user(self, user_id)\n\n    def give_money(self, ptt_id: str, money: int, red_bag_title: Optional[str] = None,\n                   red_bag_content: Optional[str] = None) -> None:\n\n        \"\"\"\n        轉帳，詳見 `P 幣`_。\n\n        .. _`P 幣`: https://pttpedia.fandom.com/zh/wiki/P%E5%B9%A3\n\n        Args:\n            ptt_id (str): PTT ID。\n            money (int): 轉帳金額。\n            red_bag_title (str): 紅包標題。\n            red_bag_content (str): 紅包內容。\n\n        Returns:\n            None\n\n        Raises:\n            RequireLogin: 需要登入。\n            UnregisteredUser: 未註冊使用者。\n            NoSuchUser: 使用者不存在。\n            NoMoney: 餘額不足。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.give_money(ptt_id='CodingMan', money=100)\n                # or\n                ptt_bot.give_money('CodingMan', 100, red_bag_title='紅包袋標題', red_bag_content='紅包袋內文')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        \"\"\"\n\n        _api_give_money.give_money(self, ptt_id, money, red_bag_title, red_bag_content)\n\n    def mail(self, ptt_id: str, title: str, content: str, sign_file: [int | str] = 0,\n             backup: bool = True) -> None:\n\n        \"\"\"\n        寄信。\n\n        Args:\n            ptt_id (str): PTT ID。\n            title (str): 信件標題。\n            content (str): 信件內容。\n            sign_file (str | int): 編號或隨機簽名檔 (x)，預設為 0 (不選)。\n            backup (bool): 如果是 True 寄信時將會備份信件，預設為 True。\n\n        Returns:\n            None\n\n        Raises:\n            RequireLogin: 需要登入。\n            UnregisteredUser: 未註冊使用者。\n            NoSuchUser: 使用者不存在。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.mail(ptt_id='CodingMan', title='信件標題', content='信件內容')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n        \"\"\"\n\n        _api_mail.mail(self, ptt_id, title, content, sign_file, backup)\n\n    def get_all_boards(self) -> List[str]:\n\n        \"\"\"\n        取得全站看板清單。\n\n        Returns:\n            List[str]，看板清單。\n\n        Raises:\n            RequireLogin: 需要登入。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                board_list = ptt_bot.get_all_boards()\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n        \"\"\"\n\n        return _api_get_board_list.get_board_list(self)\n\n    def reply_post(self, reply_to: data_type.ReplyTo, board: str, content: str, sign_file: [str | int] = 0,\n                   aid: Optional[str] = None, index: int = 0) -> None:\n\n        \"\"\"\n        回覆文章。\n\n        Args:\n            reply_to (:ref:`reply-to`): 回覆類型。\n            board (str): 看板名稱。\n            content (str): 回覆內容。\n            sign_file (str | int): 編號或隨機簽名檔 (x)，預設為 **0** (不選)。\n            aid: 文章編號。\n            index: 文章編號。\n\n        Returns:\n            None\n\n        Raises:\n            RequireLogin: 需要登入。\n            NoSuchBoard: 看板不存在。\n            NoSuchPost: 文章不存在。\n            NoPermission: 沒有回覆權限。\n            CantResponse: 已結案並標記, 不得回應。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.reply_post(reply_to=PyPtt.ReplyTo.BOARD, board='Test', content='PyPtt 程式回覆測試', index=123)\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        參考 :ref:`回覆類型 <reply-to>`、:ref:`取得最新文章編號 <api-get-newest-index>`\n        \"\"\"\n\n        _api_reply_post.reply_post(self, reply_to, board, content, sign_file, aid, index)\n\n    def set_board_title(self, board: str, new_title: str) -> None:\n        \"\"\"\n        設定看板標題。\n\n        Args:\n            board (str): 看板名稱。\n            new_title (str): 新標題。\n\n        Returns:\n            None\n\n        Raises:\n            RequireLogin: 需要登入。\n            UnregisteredUser: 未註冊使用者。\n            NoSuchBoard: 看板不存在。\n            NeedModeratorPermission: 需要看板管理員權限。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.set_board_title(board='Test', new_title='現在時間 %s' % datetime.datetime.now())\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        \"\"\"\n\n        _api_set_board_title.set_board_title(self, board, new_title)\n\n    def mark_post(self, mark_type: int, board: str, aid: Optional[str] = None, index: int = 0, search_type: int = 0,\n                  search_condition: Optional[str] = None) -> None:\n        \"\"\"\n        標記文章。\n\n        Args:\n            mark_type (:ref:`mark-type`): 標記類型。\n            board (str): 看板名稱。\n            aid (str): 文章編號。\n            index (int): 文章編號。\n            search_type (:ref:`search-type`): 搜尋類型。\n            search_condition (str): 搜尋條件。\n\n        Returns:\n            None\n\n        Raises:\n            RequireLogin: 需要登入。\n            UnregisteredUser: 未註冊使用者。\n            NoSuchBoard: 看板不存在。\n            NoSuchPost: 文章不存在。\n            NeedModeratorPermission: 需要看板管理員權限。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.mark_post(mark_type=PyPtt.MarkType.M, board='Test', index=123)\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n        \"\"\"\n\n        _api_mark_post.mark_post(self, mark_type, board, aid, index, search_type, search_condition)\n\n    def get_favourite_boards(self) -> List[dict]:\n        \"\"\"\n        取得我的最愛清單。\n\n        Returns:\n            List[dict]，收藏看板清單，詳見 :ref:`favorite-board-field`。\n\n        Raises:\n            RequireLogin: 需要登入。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n\n            try:\n                # .. login ..\n                favourite_boards = ptt_bot.get_favourite_boards()\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n        \"\"\"\n\n        return _api_get_favourite_board.get_favourite_board(self)\n\n    def bucket(self, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:\n        \"\"\"\n        水桶。\n\n        Args:\n            board (str): 看板名稱。\n            bucket_days (int): 水桶天數。\n            reason (str): 水桶原因。\n            ptt_id (str): PTT ID。\n\n        Returns:\n            None\n\n        Raises:\n            RequireLogin: 需要登入。\n            UnregisteredUser: 未註冊使用者。\n            NoSuchBoard: 看板不存在。\n            NoSuchUser: 使用者不存在。\n            NeedModeratorPermission: 需要看板管理員權限。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.bucket(board='Test', bucket_days=7, reason='PyPtt 程式水桶測試', ptt_id='test')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        \"\"\"\n\n        _api_bucket.bucket(self, board, bucket_days, reason, ptt_id)\n\n    def search_user(self, ptt_id: str, min_page: Optional[int] = None, max_page: Optional[int] = None) -> List[str]:\n        \"\"\"\n        搜尋使用者。\n\n        Args:\n            ptt_id (str): PTT ID。\n            min_page (int): 最小頁數。\n            max_page (int): 最大頁數。\n\n        Returns:\n            List[str]，搜尋結果。\n\n        Raises:\n            RequireLogin: 需要登入。\n            UnregisteredUser: 未註冊使用者。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                search_result = ptt_bot.search_user(ptt_id='Coding')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n        \"\"\"\n\n        return _api_search_user.search_user(self, ptt_id, min_page, max_page)\n\n    def get_board_info(self, board: str, get_post_types: bool = False) -> Dict:\n        \"\"\"\n        取得看板資訊。\n\n        Args:\n            board (str): 看板名稱。\n            get_post_types (bool): 是否取得文章類型，例如：八卦板的「問卦」。\n\n        Returns:\n            Dict，看板資訊，詳見 :ref:`board-field`。\n\n        Raises:\n            RequireLogin: 需要登入。\n            NoSuchBoard: 看板不存在。\n            NoPermission: 沒有權限。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                board_info = ptt_bot.get_board_info(board='Test')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n        \"\"\"\n\n        return _api_get_board_info.get_board_info(self, board, get_post_types, call_by_others=False)\n\n    def get_mail(self, index: int, search_type: Optional[data_type.SearchType] = None,\n                 search_condition: Optional[str] = None,\n                 search_list: Optional[list] = None) -> Dict:\n        \"\"\"\n        取得信件。\n\n        Args:\n            index (int): 信件編號。\n            search_type (:ref:`search-type`): 搜尋類型。\n            search_condition: 搜尋條件。\n            search_list: 搜尋清單。\n\n        Returns:\n            Dict，信件資訊，詳見 :ref:`mail-field`。\n\n        Raises:\n            RequireLogin: 需要登入。\n            NoSuchMail: 信件不存在。\n            NoPermission: 沒有權限。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                mail = ptt_bot.get_mail(index=1)\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        參考 :doc:`get_newest_index`\n        \"\"\"\n\n        return _api_mail.get_mail(self, index, search_type, search_condition, search_list)\n\n    def del_mail(self, index: int) -> None:\n        \"\"\"\n        刪除信件。\n\n        Args:\n            index (int): 信件編號。\n\n        Returns:\n            None\n\n        Raises:\n            RequireLogin: 需要登入。\n            UnregisteredUser: 未註冊使用者。\n            MailboxFull: 信箱已滿。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.del_mail(index=1)\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n\n        參考 :doc:`get_newest_index`\n        \"\"\"\n\n        _api_mail.del_mail(self, index)\n\n    def change_pw(self, new_password: str) -> None:\n        \"\"\"\n        更改密碼。\n        備註：因批踢踢系統限制，最長密碼為 8 碼。\n\n        Args:\n            new_password (str): 新密碼。\n\n        Returns:\n            None\n\n        Raises:\n            RequireLogin: 需要登入。\n            SetContactMailFirst: 需要先設定聯絡信箱。\n            WrongPassword: 密碼錯誤。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.change_pw(new_password='123456')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n        \"\"\"\n\n        _api_change_pw.change_pw(self, new_password)\n\n    @functools.lru_cache(maxsize=64)\n    def get_aid_from_url(self, url: str) -> Tuple[str, str]:\n        \"\"\"\n        從網址取得看板名稱與文章編號。\n\n        Args:\n            url: 網址。\n\n        Returns:\n            Tuple[str, str]，看板名稱與文章編號。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            url = 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html'\n            board, aid = ptt_bot.get_aid_from_url(url)\n        \"\"\"\n\n        return lib_util.get_aid_from_url(url)\n\n    def get_bottom_post_list(self, board: str) -> List[str]:\n        \"\"\"\n        取得看板置底文章清單。\n\n        Args:\n            board (str): 看板名稱。\n\n        Returns:\n            List[post]，置底文章清單，詳見 :ref:`post-field`。\n\n        Raises:\n            RequireLogin: 需要登入。\n            NoSuchBoard: 看板不存在。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n\n            try:\n                # .. login ..\n                bottom_post_list = ptt_bot.get_bottom_post_list(board='Python')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n        \"\"\"\n\n        return _api_get_bottom_post_list.get_bottom_post_list(self, board)\n\n    def del_post(self, board: str, aid: Optional[str] = None, index: int = 0) -> None:\n        \"\"\"\n        刪除文章。\n\n        Args:\n            board (str): 看板名稱。\n            aid (str): 文章編號。\n            index (int): 文章編號。\n\n        Returns:\n            None\n\n        Raises:\n            RequireLogin: 需要登入。\n            UnregisteredUser: 未註冊使用者。\n            NoSuchBoard: 看板不存在。\n            NoSuchPost: 文章不存在。\n            NoPermission: 沒有權限。\n\n        範例::\n\n            import PyPtt\n\n            ptt_bot = PyPtt.API()\n            try:\n                # .. login ..\n                ptt_bot.del_post(board='Python', aid='1TJH_XY0')\n                # .. do something ..\n            finally:\n                ptt_bot.logout()\n        \"\"\"\n\n        _api_del_post.del_post(self, board, aid, index)\n\n    def fast_post_step0(self, board: str, title: str, content: str, post_type: int) -> None:\n        _api_post.fast_post_step0(self, board, title, content, post_type)\n\n    def fast_post_step1(self, sign_file):\n        _api_post.fast_post_step1(self, sign_file)\n\n\nif __name__ == '__main__':\n    print('PyPtt v ' + __version__)\n    print('Maintained by CodingMan')\n"
  },
  {
    "path": "PyPtt/__init__.py",
    "content": "__version__ = '1.1.2'\n\nfrom .PTT import API\nfrom .data_type import *\nfrom .exceptions import *\nfrom .log import LogLevel\nfrom .service import Service\n\nLOG_LEVEL = LogLevel\n\n_main_version = '1.2'\n"
  },
  {
    "path": "PyPtt/_api_bucket.py",
    "content": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import screens\n\n\ndef bucket(api, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    check_value.check_type(board, str, 'board')\n    check_value.check_type(bucket_days, int, 'bucket_days')\n    check_value.check_type(reason, str, 'reason')\n    check_value.check_type(ptt_id, str, 'ptt_id')\n\n    api.get_user(ptt_id)\n\n    _api_util.check_board(api, board, check_moderator=True)\n\n    _api_util.goto_board(api, board)\n\n    cmd_list = []\n    cmd_list.append('i')\n    cmd_list.append(command.ctrl_p)\n    cmd_list.append('w')\n    cmd_list.append(command.enter)\n    cmd_list.append('a')\n    cmd_list.append(command.enter)\n    cmd_list.append(ptt_id)\n    cmd_list.append(command.enter)\n    cmd = ''.join(cmd_list)\n\n    cmd_list = []\n    cmd_list.append(str(bucket_days))\n    cmd_list.append(command.enter)\n    cmd_list.append(reason)\n    cmd_list.append(command.enter)\n    cmd_list.append('y')\n    cmd_list.append(command.enter)\n    cmd_part2 = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('◆ 使用者之前已被禁言', exceptions_=exceptions.UserHasPreviouslyBeenBanned()),\n        connect_core.TargetUnit('請以數字跟單位(預設為天)輸入期限', response=cmd_part2),\n        connect_core.TargetUnit('其它鍵結束', response=command.enter),\n        connect_core.TargetUnit('權限設定系統', response=command.enter),\n        connect_core.TargetUnit('任意鍵', response=command.space),\n        connect_core.TargetUnit(screens.Target.InBoard, break_detect=True),\n    ]\n\n    api.connect_core.send(\n        cmd,\n        target_list)\n"
  },
  {
    "path": "PyPtt/_api_call_status.py",
    "content": "from . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import log\nfrom . import screens\n\n\ndef get_call_status(api) -> None:\n    # log.py = DefaultLogger('api', api.config.log_level)\n\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append('A')\n    cmd_list.append(command.right)\n    cmd_list.append(command.left)\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('[呼叫器]打開', log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit('[呼叫器]拔掉', log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit('[呼叫器]防水', log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit('[呼叫器]好友', log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit('[呼叫器]關閉', log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit('★', log_level=log.DEBUG, response=cmd),\n    ]\n\n    for i in range(2):\n        index = api.connect_core.send(cmd, target_list)\n        if index < 0:\n            if i == 0:\n                continue\n            raise exceptions.UnknownError('UnknownError')\n\n    if index == 0:\n        return data_type.call_status.ON\n    if index == 1:\n        return data_type.call_status.UNPLUG\n    if index == 2:\n        return data_type.call_status.WATERPROOF\n    if index == 3:\n        return data_type.call_status.FRIEND\n    if index == 4:\n        return data_type.call_status.OFF\n\n    ori_screen = api.connect_core.get_screen_queue()[-1]\n    raise exceptions.UnknownError(ori_screen)\n\n\ndef set_call_status(api, call_status) -> None:\n    # 打開 -> 拔掉 -> 防水 -> 好友 -> 關閉\n\n    current_call_status = api._get_call_status()\n\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append(command.ctrl_u)\n    cmd_list.append('p')\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit(screens.Target.InUserList, break_detect=True)]\n\n    while current_call_status != call_status:\n        api.connect_core.send(\n            cmd,\n            target_list,\n            screen_timeout=api.config.screen_long_timeout)\n\n        current_call_status = api._get_call_status()\n"
  },
  {
    "path": "PyPtt/_api_change_pw.py",
    "content": "from . import command, _api_util\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import log\n\n\ndef change_pw(api, new_password: str) -> None:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    log.logger.info(i18n.change_pw)\n\n    new_password = new_password[:8]\n\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append('U')\n    cmd_list.append(command.enter)\n    cmd_list.append('I')\n    cmd_list.append(command.enter)\n    cmd_list.append('2')\n    cmd_list.append(command.enter)\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('設定聯絡信箱後才能修改密碼', exceptions_=exceptions.SetContactMailFirst()),\n        connect_core.TargetUnit('您輸入的密碼不正確', exceptions_=exceptions.WrongPassword()),\n        connect_core.TargetUnit('請您確定(Y/N)？', response='Y' + command.enter),\n        connect_core.TargetUnit('檢查新密碼', response=new_password + command.enter, max_match=1),\n        connect_core.TargetUnit('設定新密碼', response=new_password + command.enter, max_match=1),\n        connect_core.TargetUnit('輸入原密碼', response=api._ptt_pw + command.enter, max_match=1),\n        connect_core.TargetUnit('設定個人資料與密碼', break_detect=True)\n    ]\n\n    index = api.connect_core.send(\n        cmd,\n        target_list,\n        screen_timeout=api.config.screen_long_timeout)\n    if index < 0:\n        ori_screen = api.connect_core.get_screen_queue()[-1]\n        raise exceptions.UnknownError(ori_screen)\n\n    api._ptt_pw = new_password\n\n    log.logger.info(i18n.change_pw, '...', i18n.success)\n"
  },
  {
    "path": "PyPtt/_api_comment.py",
    "content": "import collections\nimport time\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\nfrom . import screens\n\ncomment_option = [\n    None,\n    data_type.CommentType.PUSH,\n    data_type.CommentType.BOO,\n    data_type.CommentType.ARROW,\n]\n\n\ndef _comment(api,\n             board: str,\n             push_type: data_type.CommentType,\n             push_content: str,\n             post_aid: str,\n             post_index: int) -> None:\n    _api_util.goto_board(api, board)\n\n    cmd_list = []\n\n    if post_aid is not None:\n        cmd_list.append(lib_util.check_aid(post_aid))\n    elif post_index != 0:\n        cmd_list.append(str(post_index))\n    else:\n        raise ValueError('post_aid and post_index cannot be None at the same time')\n\n    cmd_list.append(command.enter)\n    cmd_list.append(command.comment)\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('您覺得這篇', log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit(f'→ {api.ptt_id}: ', log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit('加註方式', log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit('禁止快速連續推文', log_level=log.INFO, break_detect=True,\n                                exceptions_=exceptions.NoFastComment()),\n        connect_core.TargetUnit('禁止短時間內大量推文', log_level=log.INFO, break_detect=True,\n                                exceptions_=exceptions.NoFastComment()),\n        connect_core.TargetUnit('使用者不可發言', log_level=log.INFO, break_detect=True,\n                                exceptions_=exceptions.NoPermission(i18n.no_permission)),\n        connect_core.TargetUnit('◆ 抱歉, 禁止推薦', log_level=log.INFO, break_detect=True,\n                                exceptions_=exceptions.CantComment()),\n    ]\n\n    index = api.connect_core.send(\n        cmd,\n        target_list)\n\n    if index == -1:\n        raise exceptions.UnknownError('unknown error in comment')\n\n    log.logger.debug(i18n.has_comment_permission)\n\n    cmd_list = []\n\n    if index == 0 or index == 1:\n        push_option_line = api.connect_core.get_screen_queue()[-1]\n        push_option_line = push_option_line.split('\\n')[-1]\n\n        log.logger.debug('comment option line', push_option_line)\n\n        available_push_type = collections.defaultdict(lambda: False)\n        first_available_push_type = None\n\n        if '值得推薦' in push_option_line:\n            available_push_type[data_type.CommentType.PUSH] = True\n\n            if first_available_push_type is None:\n                first_available_push_type = data_type.CommentType.PUSH\n\n        if '只加→註解' in push_option_line:\n            available_push_type[data_type.CommentType.ARROW] = True\n\n            if first_available_push_type is None:\n                first_available_push_type = data_type.CommentType.ARROW\n\n        if '給它噓聲' in push_option_line:\n            available_push_type[data_type.CommentType.BOO] = True\n\n            if first_available_push_type is None:\n                first_available_push_type = data_type.CommentType.BOO\n\n        log.logger.debug('available_push_type', available_push_type)\n\n        if available_push_type[push_type] is False:\n            if first_available_push_type:\n                push_type = first_available_push_type\n\n        if True in available_push_type.values():\n            cmd_list.append(str(comment_option.index(push_type)))\n\n    cmd_list.append(push_content)\n    cmd_list.append(command.enter)\n    cmd_list.append('y')\n    cmd_list.append(command.enter)\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),\n    ]\n\n    api.connect_core.send(\n        cmd,\n        target_list)\n\n\ndef comment(api, board: str, push_type: data_type.CommentType, push_content: str, post_aid: str,\n            post_index: int) -> None:\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    check_value.check_type(board, str, 'board')\n\n    if not isinstance(push_type, data_type.CommentType):\n        raise TypeError(f'CommentType must be data_type.CommentType')\n\n    check_value.check_type(push_content, str, 'push_content')\n    if post_aid is not None:\n        check_value.check_type(post_aid, str, 'aid')\n    check_value.check_type(post_index, int, 'index')\n\n    if len(board) == 0:\n        raise ValueError(f'wrong parameter board: {board}')\n\n    if post_index != 0 and isinstance(post_aid, str):\n        raise ValueError('wrong parameter index and aid can\\'t both input')\n\n    if post_index == 0 and post_aid is None:\n        raise ValueError('wrong parameter index or aid must input')\n\n    if post_index != 0:\n        newest_index = api.get_newest_index(\n            data_type.NewIndex.BOARD,\n            board=board)\n        check_value.check_index('index', post_index, newest_index)\n\n    _api_util.check_board(api, board)\n\n    board_info = api._board_info_list[board.lower()]\n\n    if board_info[data_type.BoardField.is_comment_record_ip]:\n        log.logger.debug(i18n.record_ip)\n        if board_info[data_type.BoardField.is_comment_aligned]:\n            log.logger.debug(i18n.push_aligned)\n            max_push_length = 32\n        else:\n            log.logger.debug(i18n.not_push_aligned)\n            max_push_length = 43 - len(api.ptt_id)\n    else:\n        log.logger.debug(i18n.not_record_ip)\n        if board_info[data_type.BoardField.is_comment_aligned]:\n            log.logger.debug(i18n.push_aligned)\n            max_push_length = 46\n        else:\n            log.logger.debug(i18n.not_push_aligned)\n            max_push_length = 58 - len(api.ptt_id)\n\n    push_content = push_content.strip()\n\n    push_list = []\n    while push_content:\n        index = 0\n        jump = 0\n\n        while len(push_content[:index].encode('big5uao', 'replace')) < max_push_length:\n\n            if index == len(push_content):\n                break\n            if push_content[index] == '\\n':\n                jump = 1\n                break\n\n            index += 1\n\n        push_list.append(push_content[:index])\n        push_content = push_content[index + jump:]\n\n    push_list = filter(None, push_list)\n\n    for comment in push_list:\n\n        log.logger.info(i18n.comment)\n\n        for _ in range(2):\n            try:\n                _comment(api, board, push_type, comment, post_aid=post_aid, post_index=post_index)\n                break\n            except exceptions.NoFastComment:\n                # screens.show(api.config, api.connect_core.getScreenQueue())\n                log.logger.info(i18n.wait_for_no_fast_comment)\n                time.sleep(5.2)\n\n        log.logger.info(i18n.comment, '...', i18n.success)\n"
  },
  {
    "path": "PyPtt/_api_del_post.py",
    "content": "from __future__ import annotations\n\nfrom typing import Optional\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\nfrom . import screens\n\n\ndef del_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0) -> None:\n    _api_util.one_thread(api)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    check_value.check_type(board, str, 'board')\n    if post_aid is not None:\n        check_value.check_type(post_aid, str, 'PostAID')\n    check_value.check_type(post_index, int, 'PostIndex')\n\n    if len(board) == 0:\n        raise ValueError(f'board error parameter: {board}')\n\n    if post_index != 0 and isinstance(post_aid, str):\n        raise ValueError('wrong parameter index and aid can\\'t both input')\n\n    if post_index == 0 and post_aid is None:\n        raise ValueError('wrong parameter index or aid must input')\n\n    if post_index != 0:\n        newest_index = api.get_newest_index(\n            data_type.NewIndex.BOARD,\n            board=board)\n        check_value.check_index(\n            'PostIndex',\n            post_index,\n            newest_index)\n\n    log.logger.info(i18n.delete_post)\n\n    board_info = _api_util.check_board(api, board)\n\n    check_author = True\n    for moderator in board_info[data_type.BoardField.moderators]:\n        if api.ptt_id.lower() == moderator.lower():\n            check_author = False\n            break\n\n    log.logger.info(i18n.delete_post)\n\n    post_info = api.get_post(board, aid=post_aid, index=post_index, query=True)\n    if post_info[data_type.PostField.post_status] != data_type.PostStatus.EXISTS:\n        # delete success\n        log.logger.info(i18n.success)\n        return\n\n    if check_author:\n        if api.ptt_id.lower() != post_info[data_type.PostField.author].lower():\n            log.logger.info(i18n.delete_post, '...', i18n.fail)\n            raise exceptions.NoPermission(i18n.no_permission)\n\n    _api_util.goto_board(api, board)\n\n    cmd_list = []\n\n    if post_aid is not None:\n        cmd_list.append(lib_util.check_aid(post_aid))\n    elif post_index != 0:\n        cmd_list.append(str(post_index))\n    else:\n        raise ValueError('post_aid and post_index cannot be None at the same time')\n\n    cmd_list.append(command.enter)\n    cmd_list.append('d')\n\n    cmd = ''.join(cmd_list)\n\n    api.confirm = False\n\n    def confirm_delete_handler(screen):\n        api.confirm = True\n\n    target_list = [\n        connect_core.TargetUnit('請按任意鍵繼續', response=' '),\n        connect_core.TargetUnit('請確定刪除(Y/N)?[N]', response='y' + command.enter, handler=confirm_delete_handler,\n                                max_match=1),\n        connect_core.TargetUnit(screens.Target.InBoard, break_detect=True),\n    ]\n\n    index = api.connect_core.send(\n        cmd,\n        target_list)\n\n    if index == 1:\n        if not api.confirm:\n            log.logger.info(i18n.delete_post, '...', i18n.fail)\n            raise exceptions.NoPermission(i18n.no_permission)\n\n    log.logger.info(i18n.delete_post, '...', i18n.success)"
  },
  {
    "path": "PyPtt/_api_get_board_info.py",
    "content": "import re\nfrom typing import Dict\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import log\nfrom . import screens\nfrom .data_type import BoardField\n\n\ndef get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -> Dict:\n    logger = log.init(log.DEBUG if call_by_others else log.INFO)\n\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    check_value.check_type(board, str, 'board')\n\n    logger.info(\n        i18n.replace(i18n.get_board_info, board))\n\n    _api_util.goto_board(api, board, refresh=True)\n\n    ori_screen = api.connect_core.get_screen_queue()[-1]\n    # print(ori_screen)\n\n    nuser = None\n    for line in ori_screen.split('\\n'):\n        if '編號' not in line:\n            continue\n        if '日 期' not in line:\n            continue\n        if '人氣' not in line:\n            continue\n\n        nuser = line\n        break\n\n    if nuser is None:\n        raise exceptions.NoSuchBoard(api.config, board)\n\n    # print('------------------------')\n    # print('nuser', nuser)\n    # print('------------------------')\n    if '[靜]' in nuser:\n        online_user = 0\n    else:\n        if '編號' not in nuser or '人氣' not in nuser:\n            raise exceptions.NoSuchBoard(api.config, board)\n        pattern = re.compile('[\\d]+')\n        r = pattern.search(nuser)\n        if r is None:\n            raise exceptions.NoSuchBoard(api.config, board)\n        # 減一是把自己本身拿掉\n        online_user = int(r.group(0)) - 1\n\n    logger.debug('人氣', online_user)\n\n    target_list = [\n        connect_core.TargetUnit('任意鍵繼續', log_level=log.DEBUG if call_by_others else log.INFO,\n                                break_detect=True),\n    ]\n\n    api.connect_core.send(\n        'i',\n        target_list)\n\n    ori_screen = api.connect_core.get_screen_queue()[-1]\n    # print(ori_screen)\n\n    p = re.compile('《(.+)》看板設定')\n    r = p.search(ori_screen)\n    if r is not None:\n        boardname = r.group(0)[1:-5].strip()\n\n    logger.debug('看板名稱', boardname, board)\n\n    if boardname.lower() != board.lower():\n        raise exceptions.NoSuchBoard(api.config, board)\n\n    p = re.compile('中文敘述: (.+)')\n    r = p.search(ori_screen)\n    if r is not None:\n        chinese_des = r.group(0)[5:].strip()\n    logger.debug('中文敘述', chinese_des)\n\n    p = re.compile('板主名單: (.+)')\n    r = p.search(ori_screen)\n    if r is not None:\n        moderator_line = r.group(0)[5:].strip()\n        if '(無)' in moderator_line:\n            moderators = []\n        else:\n            moderators = moderator_line.split('/')\n            for moderator in moderators.copy():\n                if moderator == '徵求中':\n                    moderators.remove(moderator)\n    logger.debug('板主名單', moderators)\n\n    open_status = ('公開狀態(是否隱形): 公開' in ori_screen)\n    logger.debug('公開狀態', open_status)\n\n    into_top_ten_when_hide = ('隱板時 可以 進入十大排行榜' in ori_screen)\n    logger.debug('隱板時可以進入十大排行榜', into_top_ten_when_hide)\n\n    non_board_members_post = ('開放 非看板會員發文' in ori_screen)\n    logger.debug('非看板會員發文', non_board_members_post)\n\n    reply_post = ('開放 回應文章' in ori_screen)\n    logger.debug('回應文章', reply_post)\n\n    self_del_post = ('開放 自刪文章' in ori_screen)\n    logger.debug('自刪文章', self_del_post)\n\n    push_post = ('開放 推薦文章' in ori_screen)\n    logger.debug('推薦文章', push_post)\n\n    boo_post = ('開放 噓文' in ori_screen)\n    logger.debug('噓文', boo_post)\n\n    # 限制 快速連推文章, 最低間隔時間: 5 秒\n    # 開放 快速連推文章\n\n    fast_push = ('開放 快速連推文章' in ori_screen)\n    logger.debug('快速連推文章', fast_push)\n\n    if not fast_push:\n        p = re.compile('最低間隔時間: [\\d]+')\n        r = p.search(ori_screen)\n        if r is not None:\n            min_interval = r.group(0)[7:].strip()\n            min_interval = int(min_interval)\n        else:\n            min_interval = 0\n        logger.debug('最低間隔時間', min_interval)\n    else:\n        min_interval = 0\n\n    # 推文時 自動 記錄來源 IP\n    # 推文時 不會 記錄來源 IP\n    push_record_ip = ('推文時 自動 記錄來源 IP' in ori_screen)\n    logger.debug('記錄來源 IP', push_record_ip)\n\n    # 推文時 對齊 開頭\n    # 推文時 不用對齊 開頭\n    push_aligned = ('推文時 對齊 開頭' in ori_screen)\n    logger.debug('對齊開頭', push_aligned)\n\n    # 板主 可 刪除部份違規文字\n    moderator_can_del_illegal_content = ('板主 可 刪除部份違規文字' in ori_screen)\n    logger.debug('板主可刪除部份違規文字', moderator_can_del_illegal_content)\n\n    # 轉錄文章 會 自動記錄，且 需要 發文權限\n    tran_post_auto_recorded_and_require_post_permissions = ('轉錄文章 會 自動記錄，且 需要 發文權限' in ori_screen)\n    logger.debug('轉錄文章 會 自動記錄，且 需要 發文權限', tran_post_auto_recorded_and_require_post_permissions)\n\n    cool_mode = ('未 設為冷靜模式' not in ori_screen)\n    logger.debug('冷靜模式', cool_mode)\n\n    require18 = ('禁止 未滿十八歲進入' in ori_screen)\n    logger.debug('禁止未滿十八歲進入', require18)\n\n    p = re.compile('登入次數 [\\d]+ 次以上')\n    r = p.search(ori_screen)\n    if r is not None:\n        require_login_time = r.group(0).split(' ')[1]\n        require_login_time = int(require_login_time)\n    else:\n        require_login_time = 0\n    logger.debug('發文限制登入次數', require_login_time)\n\n    p = re.compile('退文篇數 [\\d]+ 篇以下')\n    r = p.search(ori_screen)\n    if r is not None:\n        require_illegal_post = r.group(0).split(' ')[1]\n        require_illegal_post = int(require_illegal_post)\n    else:\n        require_illegal_post = 0\n    logger.debug('發文限制退文篇數', require_illegal_post)\n\n    kind_list = []\n    if get_post_kind:\n\n        _api_util.goto_board(api, board)\n\n        # Go certain board, then post to get post type info\n        cmd_list = []\n        cmd_list.append(command.ctrl_p)\n        cmd = ''.join(cmd_list)\n\n        target_list = [\n            connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),\n            connect_core.TargetUnit('或不選)', break_detect=True)\n        ]\n\n        index = api.connect_core.send(\n            cmd,\n            target_list)\n\n        if index == 0:\n            raise exceptions.NoPermission(i18n.no_permission)\n            # no post permission\n\n        ori_screen = api.connect_core.get_screen_queue()[-1]\n        screen_lines = ori_screen.split('\\n')\n\n        for i in screen_lines:\n            if '種類：' in i:\n                type_pattern = re.compile('\\d\\.([^\\ ]*)')\n                # 0 is not present any type that the key hold None object\n                kind_list = type_pattern.findall(i)\n                break\n\n        # Clear post status\n        cmd_list = []\n        cmd_list.append(command.ctrl_c)\n        cmd_list.append(command.ctrl_c)\n        cmd = ''.join(cmd_list)\n\n        target_list = [\n            connect_core.TargetUnit(screens.Target.InBoard, break_detect=True)\n        ]\n        api.connect_core.send(\n            cmd,\n            target_list)\n\n    logger.info(\n        i18n.replace(i18n.get_board_info, board),\n        '...', i18n.success\n    )\n\n    return {\n        BoardField.board: boardname,\n        BoardField.online_user: online_user,\n        BoardField.mandarin_des: chinese_des,\n        BoardField.moderators: moderators,\n        BoardField.open_status: open_status,\n        BoardField.into_top_ten_when_hide: into_top_ten_when_hide,\n        BoardField.can_non_board_members_post: non_board_members_post,\n        BoardField.can_reply_post: reply_post,\n        BoardField.self_del_post: self_del_post,\n        BoardField.can_comment_post: push_post,\n        BoardField.can_boo_post: boo_post,\n        BoardField.can_fast_push: fast_push,\n        BoardField.min_interval_between_comments: min_interval,\n        BoardField.is_comment_record_ip: push_record_ip,\n        BoardField.is_comment_aligned: push_aligned,\n        BoardField.can_moderators_del_illegal_content: moderator_can_del_illegal_content,\n        BoardField.does_tran_post_auto_recorded_and_require_post_permissions: tran_post_auto_recorded_and_require_post_permissions,\n        BoardField.is_cool_mode: cool_mode,\n        BoardField.is_require18: require18,\n        BoardField.require_login_time: require_login_time,\n        BoardField.require_illegal_post: require_illegal_post,\n        BoardField.post_kind_list: kind_list\n    }\n"
  },
  {
    "path": "PyPtt/_api_get_board_list.py",
    "content": "import progressbar\n\nfrom . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import log\nfrom . import screens\n\n\ndef get_board_list(api) -> list:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    cmd_list = [\n        command.go_main_menu,\n        'F',\n        command.enter,\n        'y',\n        '$']\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit(screens.Target.InBoardList, break_detect=True)\n    ]\n\n    api.connect_core.send(\n        cmd,\n        target_list,\n        screen_timeout=api.config.screen_long_timeout)\n    ori_screen = api.connect_core.get_screen_queue()[-1]\n\n    max_no = 0\n    for line in ori_screen.split('\\n'):\n        if '◎' not in line and '●' not in line:\n            continue\n\n        if line.startswith(api.cursor):\n            line = line[len(api.cursor):]\n\n        # print(f'->{line}<')\n        if '◎' in line:\n            front_part = line[:line.find('◎')]\n        else:\n            front_part = line[:line.find('●')]\n        front_part_list = [x for x in front_part.split(' ')]\n        front_part_list = list(filter(None, front_part_list))\n        # print(f'FrontPartList =>{FrontPartList}<=')\n        max_no = int(front_part_list[0].rstrip(')'))\n\n    if api.config.log_level == log.INFO:\n        pb = progressbar.ProgressBar(\n            max_value=max_no,\n            redirect_stdout=True)\n\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append('F')\n    cmd_list.append(command.enter)\n    cmd_list.append('y')\n    cmd_list.append('0')\n    cmd = ''.join(cmd_list)\n\n    board_list = []\n    while True:\n\n        api.connect_core.send(\n            cmd,\n            target_list,\n            screen_timeout=api.config.screen_long_timeout)\n\n        ori_screen = api.connect_core.get_screen_queue()[-1]\n        # print(OriScreen)\n        for line in ori_screen.split('\\n'):\n            if '◎' not in line and '●' not in line:\n                continue\n\n            if line.startswith(api.cursor):\n                line = line[len(api.cursor):]\n\n            if '◎' in line:\n                front_part = line[:line.find('◎')]\n            else:\n                front_part = line[:line.find('●')]\n            front_part_list = [x for x in front_part.split(' ')]\n            front_part_list = list(filter(None, front_part_list))\n\n            number = front_part_list[0]\n            if ')' in number:\n                number = number[:number.rfind(')')]\n            no = int(number)\n\n            board_name = front_part_list[1]\n            if board_name.startswith('ˇ'):\n                board_name = board_name[1:]\n                if len(board_name) == 0:\n                    board_name = front_part_list[2]\n\n            board_list.append(board_name)\n\n            if api.config.log_level == log.INFO:\n                pb.update(no)\n\n        if no >= max_no:\n            break\n        cmd = command.ctrl_f\n\n    if api.config.log_level == log.INFO:\n        pb.finish()\n\n    return board_list\n"
  },
  {
    "path": "PyPtt/_api_get_bottom_post_list.py",
    "content": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import i18n\nfrom . import log\nfrom . import screens\n\n\ndef get_bottom_post_list(api, board):\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    check_value.check_type(board, str, 'board')\n\n    log.logger.info(i18n.catch_bottom_post)\n\n    _api_util.check_board(api, board)\n\n    _api_util.goto_board(api, board, end=True)\n\n    last_screen = api.connect_core.get_screen_queue()[-1]\n\n    bottom_screen = [line for line in last_screen.split('\\n') if '★' in line[:8]]\n    bottom_length = len(bottom_screen)\n\n    if bottom_length == 0:\n        log.logger.info(i18n.catch_bottom_post_success)\n        return list()\n\n    cmd_list = []\n    cmd_list.append(command.query_post)\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit(\n            screens.Target.PTT1_QueryPost if api.config.host == data_type.HOST.PTT1 else screens.Target.PTT2_QueryPost,\n            log_level=log.DEBUG, break_detect=True, refresh=False),\n        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)),\n    ]\n\n    aid_list = []\n\n    result = []\n    for _ in range(bottom_length):\n        api.connect_core.send(cmd, target_list)\n        last_screen = api.connect_core.get_screen_queue()[-1]\n\n        lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index = \\\n            _api_util.parse_query_post(\n                api,\n                last_screen)\n\n        aid_list.append(post_aid)\n\n        cmd_list = []\n        cmd_list.append(command.enter)\n        cmd_list.append(command.up)\n        cmd_list.append(command.query_post)\n        cmd = ''.join(cmd_list)\n\n    aid_list.reverse()\n\n    for post_aid in aid_list:\n        current_post = api.get_post(board=board, aid=post_aid, query=True)\n        result.append(current_post)\n\n    log.logger.info(i18n.catch_bottom_post, '...', i18n.success)\n\n    return list(reversed(result))\n"
  },
  {
    "path": "PyPtt/_api_get_favourite_board.py",
    "content": "from . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import log\nfrom .data_type import FavouriteBoardField\n\n\ndef get_favourite_board(api) -> list:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    cmd_list = [command.go_main_menu, 'F', command.enter, '0']\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('選擇看板', break_detect=True)\n    ]\n\n    log.logger.info(i18n.get_favourite_board_list)\n\n    board_list = []\n    favourite_board_list = []\n    while True:\n\n        api.connect_core.send(\n            cmd,\n            target_list)\n\n        ori_screen = api.connect_core.get_screen_queue()[-1]\n        # print(OriScreen)\n        screen_buf = ori_screen\n        screen_buf = [x for x in screen_buf.split('\\n')][3:-1]\n\n        # adjust for cursor\n        screen_buf[0] = '  ' + screen_buf[0][1:]\n        screen_buf = [x for x in screen_buf]\n\n        min_len = 47\n\n        for i, line in enumerate(screen_buf):\n            if len(screen_buf[i]) == 0:\n                continue\n            if len(screen_buf[i]) <= min_len:\n                # print(f'[{ScreenBuf[i]}]')\n                screen_buf[i] = screen_buf[i] + (' ' * ((min_len + 1) - len(screen_buf[i])))\n        screen_buf = [x[10:min_len - len(x)].strip() for x in screen_buf]\n        screen_buf = list(filter(None, screen_buf))\n\n        for i, line in enumerate(screen_buf):\n            if '------------' in line:\n                continue\n\n            temp = line.strip().split(' ')\n            no_space_temp = list(filter(None, temp))\n\n            board = no_space_temp[0]\n            if board.startswith('ˇ'):\n                board = board[1:]\n\n            board_type = no_space_temp[1]\n\n            title_start_index = temp.index(board_type) + 1\n            board_title = ' '.join(temp[title_start_index:])\n            # remove ◎\n            board_title = board_title[1:]\n\n            if board in board_list:\n                log.logger.info(i18n.success)\n                return favourite_board_list\n            board_list.append(board)\n\n            favourite_board_list.append({\n                FavouriteBoardField.board: board,\n                FavouriteBoardField.type: board_type,\n                FavouriteBoardField.title: board_title})\n\n        if len(screen_buf) < 20:\n            break\n\n        cmd = command.ctrl_f\n\n    log.logger.info(i18n.get_favourite_board_list, '...', i18n.success)\n    return favourite_board_list\n"
  },
  {
    "path": "PyPtt/_api_get_newest_index.py",
    "content": "from __future__ import annotations\n\nimport re\nfrom typing import Optional\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_type, lib_util\nfrom . import exceptions\nfrom . import i18n\nfrom . import log\nfrom . import screens\n\n\ndef _get_newest_index(api) -> int:\n    last_screen = api.connect_core.get_screen_queue()[-1]\n\n    last_screen_list = last_screen.split('\\n')\n    last_screen_list = last_screen_list[3:]\n    last_screen_list = '\\n'.join([x[:9] for x in last_screen_list])\n    # print(last_screen_list)\n    all_index = re.findall(r'\\d+', last_screen_list)\n\n    if len(all_index) == 0:\n        return 0\n\n    all_index = list(map(int, all_index))\n    all_index.sort(reverse=True)\n    # print(all_index)\n\n    max_check_range = 6\n    newest_index = 0\n    for index_temp in all_index:\n        need_continue = True\n        if index_temp > max_check_range:\n            check_range = max_check_range\n        else:\n            check_range = index_temp\n        for i in range(1, check_range):\n            if str(index_temp - i) not in last_screen:\n                need_continue = False\n                break\n        if need_continue:\n            log.logger.debug(i18n.find_newest_index, index_temp)\n            newest_index = index_temp\n            break\n\n    if newest_index == 0:\n        raise exceptions.UnknownError('UnknownError')\n\n    return newest_index\n\n\ndef get_newest_index(api, index_type: data_type.NewIndex, board: Optional[str] = None,\n                     search_type: data_type.SearchType = None, search_condition: Optional[str] = None,\n                     search_list: Optional[list] = None) -> int:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if search_list is None:\n        search_list = []\n    else:\n        check_value.check_type(search_list, list, 'search_list')\n\n    if (search_type, search_condition) != (None, None):\n        search_list.insert(0, (search_type, search_condition))\n\n    for search_type, search_condition in search_list:\n        check_value.check_type(search_type, data_type.SearchType, 'search_type')\n        check_value.check_type(search_condition, str, 'search_condition')\n\n    check_value.check_type(index_type, data_type.NewIndex, 'index_type')\n\n    data_key = f'{index_type}_{board}_{search_list}'\n    if data_key in api._newest_index_data:\n        return api._newest_index_data[data_key]\n\n    if index_type == data_type.NewIndex.BOARD:\n\n        check_value.check_type(board, str, 'board')\n\n        _api_util.check_board(api, board)\n        _api_util.goto_board(api, board)\n\n        cmd_list = []\n        cmd_list.append('1')\n        cmd_list.append(command.enter)\n        cmd_list.append('$')\n\n        cmd = ''.join(cmd_list)\n\n        target_list = [\n            connect_core.TargetUnit('沒有文章...', log_level=log.DEBUG, break_detect=True),\n            connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),\n            connect_core.TargetUnit(screens.Target.MainMenu_Exiting,\n                                    exceptions_=exceptions.NoSuchBoard(api.config, board)),\n        ]\n\n        index = api.connect_core.send(cmd, target_list)\n        if index < 0:\n            raise exceptions.NoSuchBoard(api.config, board)\n\n        if index == 0:\n            return 0\n\n        normal_newest_index = _get_newest_index(api)\n\n        if search_list is not None and len(search_list) > 0:\n            target_list.insert(2,\n                               connect_core.TargetUnit(\n                                   screens.Target.InBoardWithCursor,\n                                   log_level=log.DEBUG,\n                                   break_detect=True))\n\n            cmd_list = _api_util.get_search_condition_cmd(index_type, search_list)\n\n            cmd_list.append('1')\n            cmd_list.append(command.enter)\n            cmd_list.append('$')\n            cmd = ''.join(cmd_list)\n\n            index = api.connect_core.send(cmd, target_list)\n            if index < 0:\n                raise exceptions.NoSuchBoard(api.config, board)\n\n            if index == 0:\n                return 0\n\n            newest_index = _get_newest_index(api)\n\n            if normal_newest_index == newest_index:\n                raise exceptions.NoSearchResult()\n        else:\n            newest_index = normal_newest_index\n\n    elif index_type == data_type.NewIndex.MAIL:\n\n        if not api.is_registered_user:\n            raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n        if board is not None:\n            raise ValueError('board should not input at NewIndex.MAIL.')\n\n        cmd_list = []\n        cmd_list.append(command.go_main_menu)\n        cmd_list.append(command.ctrl_z)\n        cmd_list.append('m')\n\n        cmd_list.append('1')\n        cmd_list.append(command.enter)\n        cmd_list.append('$')\n\n        cmd = ''.join(cmd_list)\n\n        target_list = [\n            connect_core.TargetUnit(screens.Target.InMailBox, break_detect=True),\n            connect_core.TargetUnit(screens.Target.CursorToGoodbye, response=cmd),\n        ]\n\n        def get_index(api):\n            current_capacity, _ = _api_util.get_mailbox_capacity(api)\n            last_screen = api.connect_core.get_screen_queue()[-1]\n            cursor_line = [x for x in last_screen.split('\\n') if x.strip().startswith(api.cursor)][0]\n\n            list_index = int(re.compile('(\\d+)').search(cursor_line).group(0))\n\n            if search_type == 0 and search_list is None:\n                if list_index > current_capacity:\n                    newest_index = list_index\n                else:\n                    newest_index = current_capacity\n            else:\n                newest_index = list_index\n\n            return newest_index\n\n        newest_index = 0\n\n        index = api.connect_core.send(\n            cmd,\n            target_list)\n\n        if index == 0:\n            normal_newest_index = get_index(api)\n\n            if search_list is not None and len(search_list) > 0:\n                target_list.insert(\n                    2,\n                    connect_core.TargetUnit(\n                        screens.Target.InMailBoxWithCursor,\n                        log_level=log.DEBUG,\n                        break_detect=True)\n                )\n\n                cmd_list = []\n                cmd_list.append(command.go_main_menu)\n                cmd_list.append(command.ctrl_z)\n                cmd_list.append('m')\n\n                cmd_list.extend(\n                    _api_util.get_search_condition_cmd(index_type, search_list)\n                )\n\n                cmd_list.append('1')\n                cmd_list.append(command.enter)\n                cmd_list.append('$')\n\n                cmd = ''.join(cmd_list)\n\n                index = api.connect_core.send(\n                    cmd,\n                    target_list)\n\n                if index in [0, 2]:\n                    newest_index = get_index(api)\n                    if normal_newest_index == newest_index:\n                        raise exceptions.NoSearchResult()\n\n            else:\n                newest_index = normal_newest_index\n\n    api._newest_index_data[data_key] = newest_index\n    return newest_index\n"
  },
  {
    "path": "PyPtt/_api_get_post.py",
    "content": "from __future__ import annotations\n\nimport json\nimport re\nimport time\nfrom typing import Dict, Optional\n\nfrom AutoStrEnum import AutoJsonEncoder\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\nfrom . import screens\nfrom .data_type import PostField, CommentField\n\n\ndef get_post(api, board: str, aid: Optional[str] = None, index: Optional[int] = None,\n             search_list: Optional[list] = None,\n             search_type: Optional[data_type.SearchType] = None,\n             search_condition: Optional[str] = None, query: bool = False) -> Dict:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    check_value.check_type(board, str, 'board')\n    if aid is not None:\n        check_value.check_type(aid, str, 'aid')\n    if index is not None:\n        check_value.check_type(index, int, 'index')\n\n    if search_list is None:\n        search_list = []\n    else:\n        check_value.check_type(search_list, list, 'search_list')\n\n    if (search_type, search_condition) != (None, None):\n        search_list.insert(0, (search_type, search_condition))\n\n    for search_type, search_condition in search_list:\n        check_value.check_type(search_type, data_type.SearchType, 'search_type')\n        check_value.check_type(search_condition, str, 'search_condition')\n\n    if len(board) == 0:\n        raise ValueError(f'board error parameter: {board}')\n\n    if index is not None and isinstance(aid, str):\n        raise ValueError('wrong parameter index and aid can\\'t both input')\n\n    if index is None and aid is None:\n        raise ValueError('wrong parameter index or aid must input')\n\n    search_cmd = None\n    if search_list is not None and len(search_list) > 0:\n        current_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board, search_list=search_list)\n        search_cmd = _api_util.get_search_condition_cmd(data_type.NewIndex.BOARD, search_list)\n    else:\n        current_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board)\n\n    if index is not None:\n        check_value.check_index('index', index, current_index)\n\n    max_retry = 2\n    post = {}\n    for i in range(max_retry):\n        try:\n            post = _get_post(api, board, aid, index, query, search_cmd)\n            if not post:\n                pass\n            elif not post[PostField.pass_format_check]:\n                pass\n            else:\n                break\n        except exceptions.UnknownError:\n            if i == max_retry - 1:\n                raise\n        except exceptions.NoSuchBoard:\n            if i == max_retry - 1:\n                raise\n\n        log.logger.debug('Wait for retry repost')\n        time.sleep(0.1)\n\n    post = json.dumps(post, cls=AutoJsonEncoder)\n    return json.loads(post)\n\n\ndef _get_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0, query: bool = False,\n              search_cmd_list: Optional[list[str]] = None) -> Dict:\n    _api_util.check_board(api, board)\n    _api_util.goto_board(api, board)\n\n    cmd_list = []\n\n    if post_aid is not None:\n        cmd_list.append(lib_util.check_aid(post_aid))\n    elif post_index != 0:\n\n        if search_cmd_list is not None:\n            cmd_list.extend(search_cmd_list)\n\n        cmd_list.append(str(max(1, post_index - 100)))\n        cmd_list.append(command.enter)\n        cmd_list.append(str(post_index))\n    else:\n        raise ValueError('post_aid and post_index cannot be None at the same time')\n\n    cmd_list.append(command.enter)\n    cmd_list.append(command.query_post)\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit(\n            screens.Target.PTT1_QueryPost if api.config.host == data_type.HOST.PTT1 else screens.Target.PTT2_QueryPost,\n            log_level=log.DEBUG, break_detect=True, refresh=False),\n        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)),\n    ]\n\n    index = api.connect_core.send(cmd, target_list)\n    ori_screen = api.connect_core.get_screen_queue()[-1]\n\n    post = {\n        PostField.board: None,\n        PostField.aid: None,\n        PostField.index: None,\n        PostField.author: None,\n        PostField.date: None,\n        PostField.title: None,\n        PostField.content: None,\n        PostField.money: None,\n        PostField.url: None,\n        PostField.ip: None,\n        PostField.comments: [],\n        PostField.post_status: data_type.PostStatus.EXISTS,\n        PostField.list_date: None,\n        PostField.has_control_code: False,\n        PostField.pass_format_check: False,\n        PostField.location: None,\n        PostField.push_number: None,\n        PostField.is_lock: False,\n        PostField.full_content: None,\n        PostField.is_unconfirmed: False}\n\n    post_author = None\n    post_title = None\n    if index < 0 or index == 1:\n        # 文章被刪除\n        log.logger.debug(i18n.post_deleted)\n        log.logger.debug('OriScreen', ori_screen)\n\n        cursor_line = [line for line in ori_screen.split(\n            '\\n') if line.startswith(api.cursor)]\n\n        if len(cursor_line) != 1:\n            raise exceptions.UnknownError(ori_screen)\n\n        cursor_line = cursor_line[0]\n        log.logger.debug('CursorLine', cursor_line)\n\n        pattern = re.compile('[\\d]+\\/[\\d]+')\n        pattern_result = pattern.search(cursor_line)\n        if pattern_result is None:\n            list_date = None\n        else:\n            list_date = pattern_result.group(0)\n            list_date = list_date[-5:]\n\n        pattern = re.compile('\\[[\\w]+\\]')\n        pattern_result = pattern.search(cursor_line)\n        if pattern_result is not None:\n            post_del_status = data_type.PostStatus.DELETED_BY_AUTHOR\n        else:\n            pattern = re.compile('<[\\w]+>')\n            pattern_result = pattern.search(cursor_line)\n            post_del_status = data_type.PostStatus.DELETED_BY_MODERATOR\n\n        # > 79843     9/11 -             □ (本文已被吃掉)<\n        # > 76060     8/28 -             □ (本文已被刪除) [weida7332]\n        # print(f'O=>{CursorLine}<')\n        if pattern_result is not None:\n            post_author = pattern_result.group(0)[1:-1]\n        else:\n            post_author = None\n            post_del_status = data_type.PostStatus.DELETED_BY_UNKNOWN\n\n        log.logger.debug('ListDate', list_date)\n        log.logger.debug('PostAuthor', post_author)\n        log.logger.debug('post_del_status', post_del_status)\n\n        post.update({\n            PostField.board: board,\n            PostField.author: post_author,\n            PostField.list_date: list_date,\n            PostField.post_status: post_del_status,\n            PostField.pass_format_check: True\n        })\n\n        return post\n\n    elif index == 0:\n\n        lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index = \\\n            _api_util.parse_query_post(\n                api,\n                ori_screen)\n\n        if lock_post:\n            post.update({\n                PostField.board: board,\n                PostField.aid: post_aid,\n                PostField.index: post_index,\n                PostField.author: post_author,\n                PostField.title: post_title,\n                PostField.url: post_web,\n                PostField.money: post_money,\n                PostField.list_date: list_date,\n                PostField.pass_format_check: True,\n                PostField.push_number: push_number,\n                PostField.is_lock: True})\n            return post\n\n    if query:\n        post.update({\n            PostField.board: board,\n            PostField.aid: post_aid,\n            PostField.index: post_index,\n            PostField.author: post_author,\n            PostField.title: post_title,\n            PostField.url: post_web,\n            PostField.money: post_money,\n            PostField.list_date: list_date,\n            PostField.pass_format_check: True,\n            PostField.push_number: push_number})\n        return post\n\n    origin_post, has_control_code = _api_util.get_content(api)\n\n    if origin_post is None:\n        log.logger.info(i18n.post_deleted)\n\n        post.update({\n            PostField.board: board,\n            PostField.aid: post_aid,\n            PostField.index: post_index,\n            PostField.author: post_author,\n            PostField.title: post_title,\n            PostField.url: post_web,\n            PostField.money: post_money,\n            PostField.list_date: list_date,\n            PostField.has_control_code: has_control_code,\n            PostField.pass_format_check: False,\n            PostField.push_number: push_number,\n            PostField.is_unconfirmed: api.Unconfirmed\n        })\n        return post\n\n    post_author_pattern_new = re.compile('作者  (.+) 看板')\n    post_author_pattern_old = re.compile('作者  (.+)')\n    board_pattern = re.compile('看板  (.+)')\n\n    post_date = None\n    post_content = None\n    ip = None\n    location = None\n    push_list = []\n\n    # 格式確認，亂改的我也沒辦法Q_Q\n    origin_post_lines = origin_post.split('\\n')\n\n    author_line = origin_post_lines[0]\n\n    if board.lower() == 'allpost':\n        board_line = author_line[author_line.find(')') + 1:]\n        pattern_result = board_pattern.search(board_line)\n        if pattern_result is not None:\n            board_temp = post_author = pattern_result.group(0)\n            board_temp = board_temp[2:].strip()\n            if len(board_temp) > 0:\n                board = board_temp\n                log.logger.debug(i18n.board, board)\n\n    pattern_result = post_author_pattern_new.search(author_line)\n    if pattern_result is not None:\n        post_author = pattern_result.group(0)\n        post_author = post_author[:post_author.rfind(')') + 1]\n    else:\n        pattern_result = post_author_pattern_old.search(author_line)\n        if pattern_result is None:\n            log.logger.info(i18n.substandard_post, i18n.author)\n\n            post.update({\n                PostField.board: board,\n                PostField.aid: post_aid,\n                PostField.index: post_index,\n                PostField.author: post_author,\n                PostField.date: post_date,\n                PostField.title: post_title,\n                PostField.url: post_web,\n                PostField.money: post_money,\n                PostField.content: post_content,\n                PostField.ip: ip,\n                PostField.comments: push_list,\n                PostField.list_date: list_date,\n                PostField.has_control_code: has_control_code,\n                PostField.pass_format_check: False,\n                PostField.location: location,\n                PostField.push_number: push_number,\n                PostField.full_content: origin_post,\n                PostField.is_unconfirmed: api.Unconfirmed, })\n\n            return post\n        post_author = pattern_result.group(0)\n        post_author = post_author[:post_author.rfind(')') + 1]\n    post_author = post_author[4:].strip()\n\n    log.logger.debug(i18n.author, post_author)\n\n    post_title_pattern = re.compile('標題  (.+)')\n\n    title_line = origin_post_lines[1]\n    pattern_result = post_title_pattern.search(title_line)\n    if pattern_result is None:\n        log.logger.info(i18n.substandard_post, i18n.title)\n\n        post.update({\n            PostField.board: board,\n            PostField.aid: post_aid,\n            PostField.index: post_index,\n            PostField.author: post_author,\n            PostField.date: post_date,\n            PostField.title: post_title,\n            PostField.url: post_web,\n            PostField.money: post_money,\n            PostField.content: post_content,\n            PostField.ip: ip,\n            PostField.comments: push_list,\n            PostField.list_date: list_date,\n            PostField.has_control_code: has_control_code,\n            PostField.pass_format_check: False,\n            PostField.location: location,\n            PostField.push_number: push_number,\n            PostField.full_content: origin_post,\n            PostField.is_unconfirmed: api.Unconfirmed, })\n\n        return post\n    post_title = pattern_result.group(0)\n    post_title = post_title[4:].strip()\n\n    log.logger.debug(i18n.title, post_title)\n\n    post_date_pattern = re.compile('時間  .{24}')\n    date_line = origin_post_lines[2]\n    pattern_result = post_date_pattern.search(date_line)\n    if pattern_result is None:\n        log.logger.info(i18n.substandard_post, i18n.date)\n\n        post.update({\n            PostField.board: board,\n            PostField.aid: post_aid,\n            PostField.index: post_index,\n            PostField.author: post_author,\n            PostField.date: post_date,\n            PostField.title: post_title,\n            PostField.url: post_web,\n            PostField.money: post_money,\n            PostField.content: post_content,\n            PostField.ip: ip,\n            PostField.comments: push_list,\n            PostField.list_date: list_date,\n            PostField.has_control_code: has_control_code,\n            PostField.pass_format_check: False,\n            PostField.location: location,\n            PostField.push_number: push_number,\n            PostField.full_content: origin_post,\n            PostField.is_unconfirmed: api.Unconfirmed, })\n\n        return post\n    post_date = pattern_result.group(0)\n    post_date = post_date[4:].strip()\n\n    log.logger.debug(i18n.date, post_date)\n\n    content_fail = True\n    if screens.Target.content_start not in origin_post:\n        # print('Type 1')\n        content_fail = True\n    else:\n        post_content = origin_post\n        post_content = post_content[\n                       post_content.find(screens.Target.content_start) + len(screens.Target.content_start) + 1:]\n        # print('Type 2')\n        # print(f'PostContent [{PostContent}]')\n        for content_end in screens.Target.content_end_list:\n            # + 3 = 把 --\\n 拿掉\n            # print(f'EC [{EC}]')\n            if content_end in post_content:\n                content_fail = False\n\n                post_content = post_content[:post_content.rfind(content_end) + 3]\n                origin_post_lines = origin_post[origin_post.find(content_end):]\n                # post_content = post_content.strip()\n                origin_post_lines = origin_post_lines.split('\\n')\n                break\n\n    if content_fail:\n        log.logger.info(i18n.substandard_post, i18n.content)\n\n        post.update({\n            PostField.board: board,\n            PostField.aid: post_aid,\n            PostField.index: post_index,\n            PostField.author: post_author,\n            PostField.date: post_date,\n            PostField.title: post_title,\n            PostField.url: post_web,\n            PostField.money: post_money,\n            PostField.content: post_content,\n            PostField.ip: ip,\n            PostField.comments: push_list,\n            PostField.list_date: list_date,\n            PostField.has_control_code: has_control_code,\n            PostField.pass_format_check: False,\n            PostField.location: location,\n            PostField.push_number: push_number,\n            PostField.full_content: origin_post,\n            PostField.is_unconfirmed: api.Unconfirmed, })\n\n        return post\n\n    log.logger.debug(i18n.content, post_content)\n\n    info_lines = [line for line in origin_post_lines if line.startswith('※') or line.startswith('◆')]\n\n    pattern = re.compile('[\\d]+\\.[\\d]+\\.[\\d]+\\.[\\d]+')\n    pattern_p2 = re.compile('[\\d]+-[\\d]+-[\\d]+-[\\d]+')\n    for line in reversed(info_lines):\n\n        log.logger.debug('IP Line', line)\n\n        # type 1\n        # ※ 編輯: CodingMan (111.243.146.98 臺灣)\n        # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 111.243.146.98 (臺灣)\n\n        # type 2\n        # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 116.241.32.178\n        # ※ 編輯: kill77845 (114.136.55.237), 12/08/2018 16:47:59\n\n        # type 3\n        # ※ 發信站: 批踢踢實業坊(ptt.cc)\n        # ◆ From: 211.20.78.69\n        # ※ 編輯: JCC             來自: 211.20.78.69         (06/20 10:22)\n        # ※ 編輯: JCC (118.163.28.150), 12/03/2015 14:25:35\n\n        pattern_result = pattern.search(line)\n        if pattern_result is not None:\n            ip = pattern_result.group(0)\n            location_temp = line[line.find(ip) + len(ip):].strip()\n            location_temp = location_temp.replace('(', '')\n            location_temp = location_temp[:location_temp.rfind(')')]\n            location_temp = location_temp.strip()\n            # print(f'=>[{LocationTemp}]')\n            if ' ' not in location_temp and len(location_temp) > 0:\n                location = location_temp\n\n                log.logger.debug('Location', location)\n            break\n\n        pattern_result = pattern_p2.search(line)\n        if pattern_result is not None:\n            ip = pattern_result.group(0)\n            ip = ip.replace('-', '.')\n            # print(f'IP -> [{IP}]')\n            break\n    if api.config.host == data_type.HOST.PTT1:\n        if ip is None:\n            log.logger.info(i18n.substandard_post, ip)\n\n            post.update({\n                PostField.board: board,\n                PostField.aid: post_aid,\n                PostField.index: post_index,\n                PostField.author: post_author,\n                PostField.date: post_date,\n                PostField.title: post_title,\n                PostField.url: post_web,\n                PostField.money: post_money,\n                PostField.content: post_content,\n                PostField.ip: ip,\n                PostField.comments: push_list,\n                PostField.list_date: list_date,\n                PostField.has_control_code: has_control_code,\n                PostField.pass_format_check: False,\n                PostField.location: location,\n                PostField.push_number: push_number,\n                PostField.full_content: origin_post,\n                PostField.is_unconfirmed: api.Unconfirmed, })\n\n            return post\n    log.logger.debug('IP', ip)\n\n    push_author_pattern = re.compile('[推|噓|→] [\\w| ]+:')\n    push_date_pattern = re.compile('[\\d]+/[\\d]+ [\\d]+:[\\d]+')\n    push_ip_pattern = re.compile('[\\d]+\\.[\\d]+\\.[\\d]+\\.[\\d]+')\n\n    push_list = []\n\n    for line in origin_post_lines:\n        if line.startswith('推'):\n            comment_type = data_type.CommentType.PUSH\n        elif line.startswith('噓 '):\n            comment_type = data_type.CommentType.BOO\n        elif line.startswith('→ '):\n            comment_type = data_type.CommentType.ARROW\n        else:\n            continue\n\n        result = push_author_pattern.search(line)\n        if result is None:\n            # 不符合推文格式\n            continue\n        push_author = result.group(0)[2:-1].strip()\n\n        log.logger.debug(i18n.comment_id, push_author)\n\n        result = push_date_pattern.search(line)\n        if result is None:\n            continue\n        push_date = result.group(0)\n        log.logger.debug(i18n.comment_date, push_date)\n\n        comment_ip = None\n        result = push_ip_pattern.search(line)\n        if result is not None:\n            comment_ip = result.group(0)\n            log.logger.debug(f'{i18n.comment} ip', comment_ip)\n\n        push_content = line[line.find(push_author) + len(push_author):]\n        # PushContent = PushContent.replace(PushDate, '')\n\n        if api.config.host == data_type.HOST.PTT1:\n            push_content = push_content[:push_content.rfind(push_date)]\n        else:\n            # → CodingMan:What is Ptt?                                       推 10/04 13:25\n            push_content = push_content[:push_content.rfind(push_date) - 2]\n        if comment_ip is not None:\n            push_content = push_content.replace(comment_ip, '')\n        push_content = push_content[push_content.find(':') + 1:].strip()\n\n        log.logger.debug(i18n.comment_content, push_content)\n\n        current_push = {\n            CommentField.type: comment_type,\n            CommentField.author: push_author,\n            CommentField.content: push_content,\n            CommentField.ip: comment_ip,\n            CommentField.time: push_date}\n        push_list.append(current_push)\n\n    post.update({\n        PostField.board: board,\n        PostField.aid: post_aid,\n        PostField.index: post_index,\n        PostField.author: post_author,\n        PostField.date: post_date,\n        PostField.title: post_title,\n        PostField.url: post_web,\n        PostField.money: post_money,\n        PostField.content: post_content,\n        PostField.ip: ip,\n        PostField.comments: push_list,\n        PostField.list_date: list_date,\n        PostField.has_control_code: has_control_code,\n        PostField.pass_format_check: True,\n        PostField.location: location,\n        PostField.push_number: push_number,\n        PostField.full_content: origin_post,\n        PostField.is_unconfirmed: api.Unconfirmed})\n\n    return post\n"
  },
  {
    "path": "PyPtt/_api_get_post_index.py",
    "content": "from . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import log\nfrom . import screens\n\n\ndef get_post_index(api, board: str, aid: str) -> int:\n    _api_util.goto_board(api, board)\n\n    cmd_list = []\n    cmd_list.append('#')\n    cmd_list.append(aid)\n    cmd_list.append(command.enter)\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('找不到這個文章代碼', log_level=log.DEBUG,\n                                exceptions_=exceptions.NoSuchPost(board, aid)),\n        # 此狀態下無法使用搜尋文章代碼(AID)功能\n        connect_core.TargetUnit('此狀態下無法使用搜尋文章代碼(AID)功能',\n                                exceptions_=exceptions.CanNotUseSearchPostCode()),\n        connect_core.TargetUnit('沒有文章...', exceptions_=exceptions.NoSuchPost(board, aid)),\n        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit(screens.Target.InBoardWithCursor, log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board))\n    ]\n\n    index = api.connect_core.send(\n        cmd,\n        target_list\n    )\n    ori_screen = api.connect_core.get_screen_queue()[-1]\n    if index < 0:\n        # print(OriScreen)\n        raise exceptions.NoSuchBoard(api.config, board)\n\n    # if index == 5:\n    #     print(OriScreen)\n    #     raise exceptions.NoSuchBoard(api.config, board)\n\n    # print(index)\n    # print(OriScreen)\n    screen_list = ori_screen.split('\\n')\n\n    line = [x for x in screen_list if x.startswith(api.cursor)]\n    line = line[0]\n    last_line = screen_list[screen_list.index(line) - 1]\n    # print(LastLine)\n    # print(line)\n\n    if '編號' in last_line and '人氣:' in last_line:\n        index = line[1:].strip()\n        index_fix = False\n    else:\n        index = last_line.strip()\n        index_fix = True\n    while '  ' in index:\n        index = index.replace('  ', ' ')\n    index_list = index.split(' ')\n    index = index_list[0]\n    if index == '★':\n        return 0\n    index = int(index)\n    if index_fix:\n        index += 1\n    # print(Index)\n    return index\n"
  },
  {
    "path": "PyPtt/_api_get_time.py",
    "content": "import re\n\nfrom . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import log\nfrom . import screens\n\npattern = re.compile('[\\d]+:[\\d][\\d]')\n\n\ndef get_time(api) -> str:\n    _api_util.one_thread(api)\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append('A')\n    cmd_list.append(command.right)\n    cmd_list.append(command.left)\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),\n    ]\n\n    index = api.connect_core.send(cmd, target_list)\n    if index != 0:\n        return None\n\n    ori_screen = api.connect_core.get_screen_queue()[-1]\n    line_list = ori_screen.split('\\n')[-3:]\n\n    # 0:00\n\n    for line in line_list:\n        if '星期' in line and '線上' in line and '我是' in line:\n            result = pattern.search(line)\n            if result is not None:\n                return result.group(0)\n    return None\n"
  },
  {
    "path": "PyPtt/_api_get_user.py",
    "content": "import json\nimport re\nfrom typing import Dict\n\nfrom AutoStrEnum import AutoJsonEncoder\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\nfrom . import screens\nfrom .data_type import UserField\n\n\ndef get_user(api, ptt_id: str) -> Dict:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    check_value.check_type(ptt_id, str, 'UserID')\n    if len(ptt_id) < 2:\n        raise ValueError(f'wrong parameter user_id: {ptt_id}')\n\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append('T')\n    cmd_list.append(command.enter)\n    cmd_list.append('Q')\n    cmd_list.append(command.enter)\n    cmd_list.append(ptt_id)\n    cmd_list.append(command.enter)\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit(screens.Target.AnyKey, break_detect=True),\n        connect_core.TargetUnit(screens.Target.InTalk, break_detect=True),\n    ]\n\n    index = api.connect_core.send(\n        cmd,\n        target_list)\n    ori_screen = api.connect_core.get_screen_queue()[-1]\n    if index == 1:\n        raise exceptions.NoSuchUser(ptt_id)\n    # PTT1\n    # 《ＩＤ暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》小康 ($73866)\n    # 《登入次數》1118 次 (同天內只計一次) 《有效文章》15 篇 (退:0)\n    # 《目前動態》閱讀文章     《私人信箱》最近無新信件\n    # 《上次上站》10/06/2019 17:29:49 Sun  《上次故鄉》111.251.231.184\n    # 《 五子棋 》 0 勝  0 敗  0 和 《象棋戰績》 0 勝  0 敗  0 和\n\n    # https://github.com/Truth0906/PTTLibrary\n\n    # 強大的 PTT 函式庫\n    # 提供您 快速 穩定 完整 的 PTT API\n\n    # 提供專業的 PTT 機器人諮詢服務\n\n    # PTT2\n    # 《ＩＤ暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》家徒四壁 ($0)\n    # 《登入次數》8 次 (同天內只計一次)  《有效文章》0 篇\n    # 《目前動態》看板列表     《私人信箱》最近無新信件\n    # 《上次上站》10/06/2019 17:27:55 Sun  《上次故鄉》111.251.231.184\n    # 《 五子棋 》 0 勝  0 敗  0 和 《象棋戰績》 0 勝  0 敗  0 和\n\n    # 《個人名片》CodingMan 目前沒有名片\n\n    lines = ori_screen.split('\\n')[1:]\n\n    def parse_user_info_from_line(line: str) -> (str, str):\n        part_0 = line[line.find('》') + 1:]\n        part_0 = part_0[:part_0.find('《')].strip()\n\n        part_1 = line[line.rfind('》') + 1:].strip()\n\n        return part_0, part_1\n\n    ptt_id, buff_1 = parse_user_info_from_line(lines[0])\n    money = int(int_list[0]) if len(int_list := re.findall(r'\\d+', buff_1)) > 0 else buff_1\n    buff_0, buff_1 = parse_user_info_from_line(lines[1])\n\n    login_count = int(re.findall(r'\\d+', buff_0)[0])\n    account_verified = ('同天內只計一次' in buff_0)\n    legal_post = int(re.findall(r'\\d+', buff_1)[0])\n\n    # PTT2 沒有退文\n    if api.config.host == data_type.HOST.PTT1:\n        illegal_post = int(re.findall(r'\\d+', buff_1)[1])\n    else:\n        illegal_post = None\n\n    activity, mail = parse_user_info_from_line(lines[2])\n    last_login_date, last_login_ip = parse_user_info_from_line(lines[3])\n    five_chess, chess = parse_user_info_from_line(lines[4])\n\n    signature_file = '\\n'.join(lines[5:-1]).strip('\\n')\n\n    log.logger.debug('ptt_id', ptt_id)\n    log.logger.debug('money', money)\n    log.logger.debug('login_count', login_count)\n    log.logger.debug('account_verified', account_verified)\n    log.logger.debug('legal_post', legal_post)\n    log.logger.debug('illegal_post', illegal_post)\n    log.logger.debug('activity', activity)\n    log.logger.debug('mail', mail)\n    log.logger.debug('last_login_date', last_login_date)\n    log.logger.debug('last_login_ip', last_login_ip)\n    log.logger.debug('five_chess', five_chess)\n    log.logger.debug('chess', chess)\n    log.logger.debug('signature_file', signature_file)\n\n    user = {\n        UserField.ptt_id: ptt_id,\n        UserField.money: money,\n        UserField.login_count: login_count,\n        UserField.account_verified: account_verified,\n        UserField.legal_post: legal_post,\n        UserField.illegal_post: illegal_post,\n        UserField.activity: activity,\n        UserField.mail: mail,\n        UserField.last_login_date: last_login_date,\n        UserField.last_login_ip: last_login_ip,\n        UserField.five_chess: five_chess,\n        UserField.chess: chess,\n        UserField.signature_file: signature_file,\n    }\n    user = json.dumps(user, cls=AutoJsonEncoder)\n    return json.loads(user)\n"
  },
  {
    "path": "PyPtt/_api_give_money.py",
    "content": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\n\n\ndef give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag_content: str) -> None:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    check_value.check_type(ptt_id, str, 'ptt_id')\n    check_value.check_type(money, int, 'money')\n\n    if red_bag_title is not None:\n        check_value.check_type(red_bag_title, str, 'red_bag_title')\n    else:\n        red_bag_title = ''\n\n    if red_bag_content is not None:\n        check_value.check_type(red_bag_content, str, 'red_bag_content')\n    else:\n        red_bag_content = ''\n\n    log.logger.info(\n        i18n.replace(i18n.give_money_to, ptt_id, money))\n\n    # Check data_type.user\n    api.get_user(ptt_id)\n\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append('P')\n    cmd_list.append(command.enter)\n    cmd_list.append('P')\n    cmd_list.append(command.enter)\n    cmd_list.append('O')\n    cmd_list.append(command.enter)\n\n    cmd = ''.join(cmd_list)\n\n    edit_red_bag_cmd_list = list()\n\n    edit_red_bag_target = connect_core.TargetUnit('要修改紅包袋嗎', response='n' + command.enter)\n    if red_bag_title != '' or red_bag_content != '':\n        edit_red_bag_cmd_list.append('y')\n        edit_red_bag_cmd_list.append(command.enter)\n        if red_bag_title != '':\n            edit_red_bag_cmd_list.append(command.down)\n            edit_red_bag_cmd_list.append(command.ctrl_y)  # remove the red_bag_title\n            edit_red_bag_cmd_list.append(command.enter)\n            edit_red_bag_cmd_list.append(command.up)\n            edit_red_bag_cmd_list.append(f'標題: {red_bag_title}')\n            # reset cursor to original position\n            edit_red_bag_cmd_list.append(command.up * 2)\n        if red_bag_content != '':\n            edit_red_bag_cmd_list.append(command.down * 4)\n            edit_red_bag_cmd_list.append(command.ctrl_y * 8)  # remove original red_bag_content\n            edit_red_bag_cmd_list.append(red_bag_content)\n        edit_red_bag_cmd_list.append(command.ctrl_x)\n\n        edit_red_bag_cmd = ''.join(edit_red_bag_cmd_list)\n        edit_red_bag_target = connect_core.TargetUnit('要修改紅包袋嗎', response=edit_red_bag_cmd)\n\n    target_list = [\n        connect_core.TargetUnit('你沒有那麼多Ptt幣喔!', break_detect=True, exceptions_=exceptions.NoMoney()),\n        connect_core.TargetUnit('金額過少，交易取消!', break_detect=True, exceptions_=exceptions.NoMoney()),\n        connect_core.TargetUnit('交易取消!', break_detect=True,\n                                exceptions_=exceptions.UnknownError(i18n.transaction_cancelled)),\n        connect_core.TargetUnit('確定進行交易嗎？', response='y' + command.enter),\n        connect_core.TargetUnit('按任意鍵繼續', break_detect=True),\n        edit_red_bag_target,\n        connect_core.TargetUnit('要修改紅包袋嗎', response=command.enter),\n        connect_core.TargetUnit('完成交易前要重新確認您的身份', response=api._ptt_pw + command.enter),\n        connect_core.TargetUnit('他是你的小主人，是否匿名？', response='n' + command.enter),\n        connect_core.TargetUnit('要給他多少Ptt幣呢?', response=command.tab + str(money) + command.enter),\n        connect_core.TargetUnit('這位幸運兒的id', response=ptt_id + command.enter),\n        connect_core.TargetUnit('認證尚未過期', response='y' + command.enter),\n        connect_core.TargetUnit('交易正在進行中', response=command.space)\n    ]\n\n    api.connect_core.send(\n        cmd,\n        target_list,\n        screen_timeout=api.config.screen_long_timeout\n    )\n\n    log.logger.info(\n        i18n.replace(i18n.give_money_to, ptt_id, money),\n        '...', i18n.success)\n"
  },
  {
    "path": "PyPtt/_api_has_new_mail.py",
    "content": "import re\n\nfrom . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import log\nfrom . import screens\n\n\ndef has_new_mail(api) -> int:\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append(command.ctrl_z)\n    cmd_list.append('m')\n    # cmd_list.append('1')\n    # cmd_list.append(command.enter)\n    cmd = ''.join(cmd_list)\n    current_capacity = None\n    plus_count = 0\n    index_pattern = re.compile('(\\d+)')\n    checked_index_list = []\n    break_detect = False\n\n    target_list = [\n        connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True)\n    ]\n\n    api.connect_core.send(\n        cmd,\n        target_list,\n    )\n    current_capacity, _ = _api_util.get_mailbox_capacity(api)\n    if current_capacity > 20:\n        cmd_list = []\n        cmd_list.append(command.go_main_menu)\n        cmd_list.append(command.ctrl_z)\n        cmd_list.append('m')\n        cmd_list.append('1')\n        cmd_list.append(command.enter)\n        cmd = ''.join(cmd_list)\n\n    while True:\n        if current_capacity > 20:\n            api.connect_core.send(\n                cmd,\n                target_list,\n            )\n        last_screen = api.connect_core.get_screen_queue()[-1]\n\n        last_screen_list = last_screen.split('\\n')\n        last_screen_list = last_screen_list[3:-1]\n        last_screen_list = [x[:10] for x in last_screen_list]\n\n        current_plus_count = 0\n        for line in last_screen_list:\n            if str(current_capacity) in line:\n                break_detect = True\n\n            index_result = index_pattern.search(line)\n            if index_result is None:\n                continue\n            current_index = index_result.group(0)\n            if current_index in checked_index_list:\n                continue\n            checked_index_list.append(current_index)\n            if '+' not in line:\n                continue\n\n            current_plus_count += 1\n\n        plus_count += current_plus_count\n        if break_detect:\n            break\n        cmd = command.ctrl_f\n\n    return plus_count\n"
  },
  {
    "path": "PyPtt/_api_loginout.py",
    "content": "import re\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import i18n\nfrom . import log\nfrom . import screens\n\n\ndef logout(api) -> None:\n    _api_util.one_thread(api)\n\n    log.logger.info(i18n.logout)\n    if not api._is_login:\n        log.logger.info(i18n.logout, '...', i18n.success)\n        return\n\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append('g')\n    cmd_list.append(command.enter)\n    cmd_list.append('y')\n    cmd_list.append(command.enter)\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('任意鍵', break_detect=True),\n    ]\n\n    try:\n        api.connect_core.send(cmd, target_list)\n        api.connect_core.close()\n    except exceptions.ConnectionClosed:\n        pass\n    except RuntimeError:\n        pass\n\n    api._is_login = False\n\n    log.logger.info(i18n.logout, '...', i18n.success)\n\n\ndef login(api, ptt_id: str, ptt_pw: str, kick_other_session: bool):\n    _api_util.one_thread(api)\n\n    check_value.check_type(ptt_id, str, 'ptt_id')\n    check_value.check_type(ptt_pw, str, 'password')\n    check_value.check_type(kick_other_session, bool, 'kick_other_session')\n\n    if api._is_login:\n        api.logout()\n\n    api.config.kick_other_session = kick_other_session\n\n    def kick_other_login_display_msg():\n        if api.config.kick_other_session:\n            return i18n.kick_other_login\n        return i18n.not_kick_other_login\n\n    def kick_other_login_response(screen):\n        if api.config.kick_other_session:\n            return 'y' + command.enter\n        return 'n' + command.enter\n\n    api.is_mailbox_full = False\n\n    # def is_mailbox_full():\n    #     log.log(\n    #         api.config,\n    #         LogLevel.INFO,\n    #         i18n.MailBoxFull)\n    #     api.is_mailbox_full = True\n\n    def register_processing(screen):\n        pattern = re.compile('[\\d]+')\n        api.process_picks = int(pattern.search(screen).group(0))\n\n    if len(ptt_pw) > 8:\n        ptt_pw = ptt_pw[:8]\n\n    ptt_id = ptt_id.strip()\n    ptt_pw = ptt_pw.strip()\n\n    api.ptt_id = ptt_id\n    api._ptt_pw = ptt_pw\n\n    api.connect_core.connect()\n\n    log.logger.info(i18n.login_id, ptt_id)\n\n    target_list = [\n        connect_core.TargetUnit(screens.Target.InMailBox,\n                                response=command.go_main_menu + 'A' + command.right + command.left, break_detect=True),\n        connect_core.TargetUnit(screens.Target.InMailMenu, response=command.go_main_menu),\n        connect_core.TargetUnit(screens.Target.MainMenu, break_detect=True),\n        connect_core.TargetUnit('【看板列表】', response=command.go_main_menu),\n        connect_core.TargetUnit('密碼不對', break_detect=True, exceptions_=exceptions.WrongIDorPassword()),\n        connect_core.TargetUnit('請重新輸入', break_detect=True, exceptions_=exceptions.WrongIDorPassword()),\n        connect_core.TargetUnit('登入太頻繁', response=' '),\n        connect_core.TargetUnit('系統過載', break_detect=True),\n        connect_core.TargetUnit('您要刪除以上錯誤嘗試的記錄嗎', response='y' + command.enter),\n        connect_core.TargetUnit('請選擇暫存檔 (0-9)[0]', response=command.enter),\n        connect_core.TargetUnit('有一篇文章尚未完成', response='Q' + command.enter),\n        connect_core.TargetUnit(\n            '請重新設定您的聯絡信箱', break_detect=True, exceptions_=exceptions.ResetYourContactEmail()),\n        # connect_core.TargetUnit(\n        #     i18n.in_login_process_please_wait,\n        #     '登入中，請稍候'),\n        connect_core.TargetUnit('密碼正確'),\n        # 密碼正確\n        connect_core.TargetUnit('您想刪除其他重複登入的連線嗎', response=kick_other_login_response),\n        connect_core.TargetUnit('◆ 您的註冊申請單尚在處理中', response=command.enter, handler=register_processing),\n        connect_core.TargetUnit('任意鍵', response=' '),\n        connect_core.TargetUnit('正在更新與同步線上使用者及好友名單'),\n        connect_core.TargetUnit('【分類看板】', response=command.go_main_menu),\n        connect_core.TargetUnit([\n            '大富翁',\n            '排行榜',\n            '名次',\n            '代號',\n            '暱稱',\n            '數目'\n        ], response=command.go_main_menu),\n        connect_core.TargetUnit([\n            '熱門話題'\n        ], response=command.go_main_menu),\n        connect_core.TargetUnit('您確定要填寫註冊單嗎', response=command.enter * 3),\n        connect_core.TargetUnit('以上資料是否正確', response='y' + command.enter),\n        connect_core.TargetUnit('另外若輸入後發生認證碼錯誤請先確認輸入是否為最後一封', response='x' + command.enter),\n        connect_core.TargetUnit('此帳號已設定為只能使用安全連線', exceptions_=exceptions.OnlySecureConnection())\n    ]\n\n    # IAC = '\\xff'\n    # WILL = '\\xfb'\n    # NAWS = '\\x1f'\n\n    cmd_list = []\n    # cmd_list.append(IAC + WILL + NAWS)\n    cmd_list.append(ptt_id + ',')\n    cmd_list.append(command.enter)\n    cmd_list.append(ptt_pw)\n    cmd_list.append(command.enter)\n\n    cmd = ''.join(cmd_list)\n\n    index = api.connect_core.send(\n        cmd,\n        target_list,\n        screen_timeout=api.config.screen_long_timeout,\n        refresh=False,\n        secret=True)\n    if index == 0:\n\n        current_capacity, max_capacity = _api_util.get_mailbox_capacity(api)\n\n        log.logger.info(i18n.has_new_mail_goto_main_menu)\n\n        if current_capacity > max_capacity:\n            api.is_mailbox_full = True\n\n            log.logger.info(i18n.mail_box_full)\n\n        if api.is_mailbox_full:\n            log.logger.info(i18n.use_mailbox_api_will_logout_after_execution)\n\n        target_list = [\n            connect_core.TargetUnit(screens.Target.MainMenu, break_detect=True)\n        ]\n\n        cmd = command.go_main_menu + 'A' + command.right + command.left\n\n        index = api.connect_core.send(\n            cmd,\n            target_list,\n            screen_timeout=api.config.screen_long_timeout,\n            secret=True)\n\n    ori_screen = api.connect_core.get_screen_queue()[-1]\n\n    is_login = True\n\n    for t in screens.Target.MainMenu:\n        if t not in ori_screen:\n            is_login = False\n            break\n\n    if not is_login:\n        raise exceptions.LoginError()\n\n    if '> (' in ori_screen:\n        api.cursor = data_type.Cursor.NEW\n        log.logger.debug(i18n.new_cursor)\n    elif '●(' in ori_screen:\n        api.cursor = data_type.Cursor.OLD\n        log.logger.debug(i18n.old_cursor)\n    else:\n        raise exceptions.UnknownError()\n\n    screens.Target.InBoardWithCursor = screens.Target.InBoardWithCursor[:screens.Target.InBoardWithCursorLen]\n    screens.Target.InBoardWithCursor.append(api.cursor)\n\n    screens.Target.InMailBoxWithCursor = screens.Target.InMailBoxWithCursor[:screens.Target.InMailBoxWithCursorLen]\n    screens.Target.InMailBoxWithCursor.append(api.cursor)\n\n    screens.Target.CursorToGoodbye = screens.Target.CursorToGoodbye[:len(screens.Target.MainMenu)]\n    if api.cursor == '>':\n        screens.Target.CursorToGoodbye.append('> (G)oodbye')\n    else:\n        screens.Target.CursorToGoodbye.append('●(G)oodbye')\n\n    unregistered_user = True\n    if '(T)alk' in ori_screen:\n        unregistered_user = False\n    if '(P)lay' in ori_screen:\n        unregistered_user = False\n    if '(N)amelist' in ori_screen:\n        unregistered_user = False\n\n    if unregistered_user:\n        log.logger.info(i18n.unregistered_user_cant_use_all_api)\n\n    api.is_registered_user = not unregistered_user\n\n    if api.process_picks != 0:\n        log.logger.info(i18n.picks_in_register, api.process_picks)\n\n    api._is_login = True\n    log.logger.info(i18n.login_success)\n"
  },
  {
    "path": "PyPtt/_api_mail.py",
    "content": "from __future__ import annotations\n\nimport re\nfrom typing import Dict, Optional\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\nfrom . import screens\nfrom .data_type import MailField\n\n\n# 寄信\ndef mail(api,\n         ptt_id: str,\n         title: str,\n         content: str,\n         sign_file,\n         backup: bool = True) -> None:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    check_value.check_type(ptt_id, str, 'ptt_id')\n    check_value.check_type(title, str, 'title')\n    check_value.check_type(content, str, 'content')\n\n    api.get_user(ptt_id)\n\n    check_sign_file = False\n    for i in range(0, 10):\n        if str(i) == sign_file or i == sign_file:\n            check_sign_file = True\n            break\n\n    if not check_sign_file:\n        if sign_file.lower() != 'x':\n            raise ValueError(f'wrong parameter sign_file: {sign_file}')\n\n    cmd_list = []\n    # 回到主選單\n    cmd_list.append(command.go_main_menu)\n    # 私人信件區\n    cmd_list.append('M')\n    cmd_list.append(command.enter)\n    # 站內寄信\n    cmd_list.append('S')\n    cmd_list.append(command.enter)\n    # 輸入 id\n    cmd_list.append(ptt_id)\n    cmd_list.append(command.enter)\n\n    cmd = ''.join(cmd_list)\n\n    # 定義如何根據情況回覆訊息\n    target_list = [\n        connect_core.TargetUnit('主題：', break_detect=True),\n        connect_core.TargetUnit('【電子郵件】', exceptions_=exceptions.NoSuchUser(ptt_id))\n    ]\n\n    api.connect_core.send(\n        cmd,\n        target_list,\n        screen_timeout=api.config.screen_long_timeout\n    )\n\n    cmd_list = []\n    # 輸入標題\n    cmd_list.append(title)\n    cmd_list.append(command.enter)\n    # 輸入內容\n    cmd_list.append(content)\n    # 儲存檔案\n    cmd_list.append(command.ctrl_x)\n\n    cmd = ''.join(cmd_list)\n\n    # 定義如何根據情況回覆訊息\n    target_list = [\n        connect_core.TargetUnit('請按任意鍵繼續', response=command.enter, break_detect_after_send=True),\n        connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),\n        connect_core.TargetUnit('是否自存底稿', response=('y' if backup else 'n') + command.enter),\n        connect_core.TargetUnit('選擇簽名檔', response=str(sign_file) + command.enter),\n        connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),\n    ]\n\n    # 送出訊息\n    api.connect_core.send(\n        cmd,\n        target_list,\n        screen_timeout=api.config.screen_post_timeout)\n\n    log.logger.info(i18n.send_mail, i18n.success)\n\n\n# --\n# ※ 發信站: 批踢踢實業坊(ptt.cc)\n# ◆ From: 220.142.14.95\ncontent_start = '───────────────────────────────────────'\ncontent_end = '--\\n※ 發信站: 批踢踢實業坊(ptt.cc)'\ncontent_ip_old = '◆ From: '\n\nmail_author_pattern = re.compile('作者  (.+)')\nmail_title_pattern = re.compile('標題  (.+)')\nmail_date_pattern = re.compile('時間  (.+)')\nip_pattern = re.compile('[\\d]+\\.[\\d]+\\.[\\d]+\\.[\\d]+')\n\n\ndef get_mail(api, index: int, search_type: Optional[data_type.SearchType] = None,\n             search_condition: Optional[str] = None,\n             search_list: Optional[list] = None) -> Dict:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    log.logger.info(i18n.get_mail)\n\n    if not isinstance(index, int):\n        raise ValueError('index must be int')\n\n    current_index = api.get_newest_index(data_type.NewIndex.MAIL)\n    if index <= 0 or current_index < index:\n        raise exceptions.NoSuchMail()\n    # check_value.check_index('index', index, current_index)\n\n    cmd_list = []\n    # 回到主選單\n    cmd_list.append(command.go_main_menu)\n    # 進入信箱\n    cmd_list.append(command.ctrl_z)\n    cmd_list.append('m')\n\n    # 處理條件整理出指令\n    _cmd_list = _api_util.get_search_condition_cmd(data_type.NewIndex.MAIL, search_list)\n\n    cmd_list.extend(_cmd_list)\n\n    # 前進至目標信件位置\n    cmd_list.append(str(index))\n    cmd_list.append(command.enter)\n    cmd = ''.join(cmd_list)\n\n    # 有時候會沒有最底下一列，只好偵測游標是否出現\n    if api.cursor == data_type.Cursor.NEW:\n        space_length = 6 - len(api.cursor) - len(str(index))\n    else:\n        space_length = 5 - len(api.cursor) - len(str(index))\n    fast_target = f\"{api.cursor}{' ' * space_length}{index}\"\n\n    # 定義如何根據情況回覆訊息\n    target_list = [\n        connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit(fast_target, log_level=log.DEBUG, break_detect=True)\n    ]\n\n    # 送出訊息\n    api.connect_core.send(\n        cmd,\n        target_list)\n\n    # 取得信件全文\n    origin_mail, _ = _api_util.get_content(api, post_mode=False)\n\n    # 使用表示式分析信件作者\n    pattern_result = mail_author_pattern.search(origin_mail)\n    if pattern_result is None:\n        mail_author = None\n    else:\n        mail_author = pattern_result.group(0)[2:].strip()\n\n    # 使用表示式分析信件標題\n    pattern_result = mail_title_pattern.search(origin_mail)\n    if pattern_result is None:\n        mail_title = None\n    else:\n        mail_title = pattern_result.group(0)[2:].strip()\n\n    # 使用表示式分析信件日期\n    pattern_result = mail_date_pattern.search(origin_mail)\n    if pattern_result is None:\n        mail_date = None\n    else:\n        mail_date = pattern_result.group(0)[2:].strip()\n\n    # 從全文拿掉信件開頭作為信件內文\n    mail_content = origin_mail[origin_mail.find(content_start) + len(content_start) + 1:]\n\n    mail_content = mail_content[\n                   mail_content.find(screens.Target.content_start) + len(screens.Target.content_start) + 1:]\n\n    for EC in screens.Target.content_end_list:\n        # + 3 = 把 --\\n 拿掉\n        if EC in mail_content:\n            mail_content = mail_content[:mail_content.rfind(EC) + 3].rstrip()\n            break\n\n    # 紅包偵測\n    red_envelope = False\n    if content_end not in origin_mail and 'Ptt幣的大紅包喔' in origin_mail:\n        mail_content = mail_content.strip()\n        red_envelope = True\n    else:\n\n        mail_content = mail_content[:mail_content.rfind(content_end) + 3]\n\n    if red_envelope:\n        mail_ip = None\n        mail_location = None\n    else:\n        # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 111.242.182.114\n        # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 59.104.127.126 (臺灣)\n\n        # 非紅包開始解析 ip 與 地區\n\n        ip_line_list = origin_mail.split('\\n')\n        ip_line = [x for x in ip_line_list if x.startswith(content_end[3:])]\n\n        if len(ip_line) == 0:\n            # 沒 ip 就沒地區\n            mail_ip = None\n            mail_location = None\n        else:\n            ip_line = ip_line[0]\n\n            result = ip_pattern.search(ip_line)\n            if result is None:\n                ip_line = [x for x in ip_line_list if x.startswith(content_ip_old)]\n\n                if len(ip_line) == 0:\n                    mail_ip = None\n                else:\n                    ip_line = ip_line[0]\n                    result = ip_pattern.search(ip_line)\n                    mail_ip = result.group(0)\n            else:\n                mail_ip = result.group(0)\n\n            location = ip_line[ip_line.find(mail_ip) + len(mail_ip):].strip()\n            if len(location) == 0:\n                mail_location = None\n            else:\n                # print(location)\n                mail_location = location[1:-1]\n\n    log.logger.info(i18n.get_mail, '...', i18n.success)\n\n    return {\n        MailField.origin_mail: origin_mail,\n        MailField.author: mail_author,\n        MailField.title: mail_title,\n        MailField.date: mail_date,\n        MailField.content: mail_content,\n        MailField.ip: mail_ip,\n        MailField.location: mail_location,\n        MailField.is_red_envelope: red_envelope}\n\n\ndef del_mail(api, index) -> None:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    current_index = api.get_newest_index(data_type.NewIndex.MAIL)\n    check_value.check_index(index, current_index)\n\n    cmd_list = []\n    # 進入主選單\n    cmd_list.append(command.go_main_menu)\n    # 進入信箱\n    cmd_list.append(command.ctrl_z)\n    cmd_list.append('m')\n    if index > 20:\n        # speed up\n        cmd_list.append(str(1))\n        cmd_list.append(command.enter)\n\n    # 前進到目標信件位置\n    cmd_list.append(str(index))\n    cmd_list.append(command.enter)\n    # 刪除\n    cmd_list.append('dy')\n    cmd_list.append(command.enter)\n    cmd = ''.join(cmd_list)\n\n    # 定義如何根據情況回覆訊息\n    target_list = [\n        connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True)\n    ]\n\n    # 送出\n    api.connect_core.send(\n        cmd,\n        target_list)\n\n    if api.is_mailbox_full:\n        api.logout()\n        raise exceptions.MailboxFull()\n"
  },
  {
    "path": "PyPtt/_api_mark_post.py",
    "content": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\nfrom . import screens\n\n\ndef mark_post(api, mark_type: int, board: str, post_aid: str, post_index: int, search_type: int,\n              search_condition: str) -> None:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    if not isinstance(mark_type, data_type.MarkType):\n        raise TypeError(f'mark_type must be data_type.MarkType')\n\n    check_value.check_type(board, str, 'board')\n    if post_aid is not None:\n        check_value.check_type(post_aid, str, 'PostAID')\n    check_value.check_type(post_index, int, 'PostIndex')\n\n    if not isinstance(search_type, data_type.SearchType):\n        raise TypeError(f'search_type must be data_type.SearchType')\n\n    if search_condition is not None:\n        check_value.check_type(search_condition, str, 'SearchCondition')\n\n    if len(board) == 0:\n        raise ValueError(f'board error parameter: {board}')\n\n    if mark_type != data_type.MarkType.DELETE_D:\n        if post_index != 0 and isinstance(post_aid, str):\n            raise ValueError('wrong parameter index and aid can\\'t both input')\n\n        if post_index == 0 and post_aid is None:\n            raise ValueError('wrong parameter index or aid must input')\n\n    if search_condition is not None and search_type == 0:\n        raise ValueError('wrong parameter index or aid must input')\n\n    if search_type == data_type.SearchType.COMMENT:\n        try:\n            S = int(search_condition)\n        except ValueError:\n            raise ValueError(f'wrong parameter search_condition: {search_condition}')\n\n        check_value.check_range(S, -100, 100, 'search_condition')\n\n    if post_aid is not None and search_condition is not None:\n        raise ValueError('wrong parameter aid and search_condition can\\'t both input')\n\n    if post_index != 0:\n        newest_index = api.get_newest_index(\n            data_type.NewIndex.BOARD,\n            board=board,\n            search_type=search_type,\n            search_condition=search_condition)\n        check_value.check_index(\n            'index',\n            post_index,\n            max_value=newest_index)\n\n    if mark_type == data_type.MarkType.UNCONFIRMED:\n        # 批踢踢兔沒有待證文章功能 QQ\n        if api.config.host == data_type.HOST.PTT2:\n            raise exceptions.HostNotSupport(lib_util.get_current_func_name())\n\n    _api_util.check_board(\n        board,\n        check_moderator=True)\n\n    _api_util.goto_board(api, board)\n\n    cmd_list = []\n    if post_aid is not None:\n        cmd_list.append(lib_util.check_aid(post_aid))\n        cmd_list.append(command.enter)\n    elif post_index != 0:\n        if search_condition is not None:\n            if search_type == data_type.SearchType.KEYWORD:\n                cmd_list.append('/')\n            elif search_type == data_type.SearchType.AUTHOR:\n                cmd_list.append('a')\n            elif search_type == data_type.SearchType.COMMENT:\n                cmd_list.append('Z')\n            elif search_type == data_type.SearchType.MARK:\n                cmd_list.append('G')\n            elif search_type == data_type.SearchType.MONEY:\n                cmd_list.append('A')\n\n            cmd_list.append(search_condition)\n            cmd_list.append(command.enter)\n\n        cmd_list.append(str(post_index))\n\n        cmd_list.append(command.enter)\n    else:\n        raise ValueError('post_aid and post_index cannot be None at the same time')\n\n    if mark_type == data_type.MarkType.S:\n        cmd_list.append('L')\n    elif mark_type == data_type.MarkType.D:\n        cmd_list.append('t')\n    elif mark_type == data_type.MarkType.DELETE_D:\n        cmd_list.append(command.ctrl_d)\n    elif mark_type == data_type.MarkType.M:\n        cmd_list.append('m')\n    elif mark_type == data_type.MarkType.UNCONFIRMED:\n        cmd_list.append(command.ctrl_e + 'S')\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('刪除所有標記', log_level=log.INFO, response='y' + command.enter),\n        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.INFO, break_detect=True),\n    ]\n\n    api.connect_core.send(cmd, target_list)\n"
  },
  {
    "path": "PyPtt/_api_post.py",
    "content": "from __future__ import annotations\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\n\n\ndef fast_post_step0(api, board: str, title: str, content: str, post_type: int) -> None:\n    _api_util.goto_board(api, board)\n\n    cmd_list = []\n    cmd_list.append(command.ctrl_p)\n    cmd_list.append(str(post_type))\n    cmd_list.append(command.enter)\n    cmd_list.append(str(title))\n    cmd_list.append(command.enter)\n    cmd_list.append(str(content))\n    cmd_list.append(command.ctrl_x)\n    cmd_list.append('s')\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('發表文章於【', break_detect=True),\n        connect_core.TargetUnit('使用者不可發言', break_detect=True),\n        connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),\n        connect_core.TargetUnit('任意鍵繼續', break_detect=True),\n        connect_core.TargetUnit('確定要儲存檔案嗎', break_detect=True)\n    ]\n    index = api.connect_core.fast_send(cmd, target_list)\n    if index < 0:\n        raise exceptions.UnknownError('UnknownError')\n    if index == 1 or index == 2:\n        raise exceptions.NoPermission(i18n.no_permission)\n\n\ndef fast_post_step1(api: object, sign_file) -> None:\n    cmd = '\\r'\n\n    target_list = [\n        connect_core.TargetUnit('發表文章於【', break_detect=True),\n        connect_core.TargetUnit('使用者不可發言', break_detect=True),\n        connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),\n        connect_core.TargetUnit('任意鍵繼續', break_detect=True),\n        connect_core.TargetUnit('確定要儲存檔案嗎', break_detect=True),\n        connect_core.TargetUnit('x=隨機', response=str(sign_file) + '\\r'),\n    ]\n    index = api.connect_core.fast_send(cmd, target_list)\n    if index < 0:\n        raise exceptions.UnknownError('UnknownError')\n\n\ndef fast_post(\n        api: object,\n        board: str,\n        title: str,\n        content: str,\n        post_type: int,\n        sign_file) -> None:\n    _api_util.goto_board(api, board)\n\n    cmd_list = []\n    cmd_list.append(command.ctrl_p)\n    cmd_list.append(str(post_type))\n    cmd_list.append(command.enter)\n    cmd_list.append(str(title))\n    cmd_list.append(command.enter)\n    cmd_list.append(str(content))\n    cmd_list.append(command.ctrl_x)\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('發表文章於【', break_detect=True),\n        connect_core.TargetUnit('使用者不可發言', break_detect=True),\n        connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),\n        connect_core.TargetUnit('任意鍵繼續', break_detect=True),\n        connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),\n        connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),\n    ]\n    index = api.connect_core.fast_send(cmd, target_list)\n    if index < 0:\n        raise exceptions.UnknownError('UnknownError')\n    if index == 1 or index == 2:\n        raise exceptions.NoPermission(i18n.no_permission)\n\n\nsign_file_list = [str(x) for x in range(0, 10)]\nsign_file_list.append('x')\n\n\ndef post(api, board: str, title: str, content: str, title_index: int, sign_file: [str | int]) -> None:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    check_value.check_type(board, str, 'board')\n    check_value.check_type(title_index, int, 'title_index')\n    check_value.check_type(title, str, 'title')\n    check_value.check_type(content, str, 'content')\n\n    if str(sign_file).lower() not in sign_file_list:\n        raise ValueError(f'wrong parameter sign_file: {sign_file}')\n\n    _api_util.check_board(api, board)\n    _api_util.goto_board(api, board)\n\n    log.logger.info(i18n.post)\n\n    cmd_list = []\n    cmd_list.append(command.ctrl_p)\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('發表文章於【', break_detect=True),\n        connect_core.TargetUnit('使用者不可發言', break_detect=True),\n        connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),\n    ]\n    index = api.connect_core.send(cmd, target_list)\n    if index < 0:\n        raise exceptions.UnknownError('UnknownError')\n    if index == 1 or index == 2:\n        log.logger.info(i18n.post, '...', i18n.fail)\n        raise exceptions.NoPermission(i18n.no_permission)\n\n    log.logger.debug(i18n.has_post_permission)\n\n    content = lib_util.uniform_new_line(content)\n\n    cmd_list = []\n    cmd_list.append(str(title_index))\n    cmd_list.append(command.enter)\n    cmd_list.append(str(title))\n    cmd_list.append(command.enter)\n    cmd_list.append(command.ctrl_y * 40)\n    cmd_list.append(str(content))\n    cmd_list.append(command.ctrl_x)\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('任意鍵繼續', break_detect=True),\n        connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),\n        connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),\n    ]\n    api.connect_core.send(\n        cmd,\n        target_list,\n        screen_timeout=api.config.screen_post_timeout)\n\n    log.logger.info(i18n.post, '...', i18n.success)\n"
  },
  {
    "path": "PyPtt/_api_reply_post.py",
    "content": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\n\n\ndef reply_post(api, reply_to: data_type.ReplyTo, board: str, content: str, sign_file, post_aid: str,\n               post_index: int) -> None:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not isinstance(reply_to, data_type.ReplyTo):\n        raise TypeError(f'ReplyTo must be data_type.ReplyTo')\n\n    check_value.check_type(board, str, 'board')\n    check_value.check_type(content, str, 'content')\n    if post_aid is not None:\n        check_value.check_type(post_aid, str, 'PostAID')\n\n    if post_index != 0:\n        newest_index = api.get_newest_index(\n            data_type.NewIndex.BOARD,\n            board=board)\n        check_value.check_index(\n            'index',\n            post_index,\n            max_value=newest_index)\n\n    sign_file_list = ['x']\n    sign_file_list.extend([str(x) for x in range(0, 10)])\n\n    if str(sign_file).lower() not in sign_file_list:\n        raise ValueError(f'wrong parameter sign_file: {sign_file}')\n\n    if post_aid is not None and post_index != 0:\n        raise ValueError('wrong parameter aid and index can\\'t both input')\n\n    _api_util.check_board(api, board)\n\n    _api_util.goto_board(api, board)\n\n    cmd_list = []\n\n    if post_aid is not None:\n        cmd_list.append(lib_util.check_aid(post_aid))\n    elif post_index != 0:\n        cmd_list.append(str(post_index))\n    else:\n        raise ValueError('post_aid and post_index cannot be None at the same time')\n\n    cmd_list.append(command.enter * 2)\n    cmd_list.append('r')\n\n    if reply_to == data_type.ReplyTo.BOARD:\n        log.logger.info(i18n.reply_board)\n        reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='F' + command.enter)\n    elif reply_to == data_type.ReplyTo.MAIL:\n        log.logger.info(i18n.reply_mail)\n        reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='M' + command.enter)\n    elif reply_to == data_type.ReplyTo.BOARD_MAIL:\n        log.logger.info(i18n.reply_board_mail)\n        reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='B' + command.enter)\n\n    cmd = ''.join(cmd_list)\n    target_list = [\n        connect_core.TargetUnit('任意鍵繼續', break_detect=True),\n        connect_core.TargetUnit('◆ 很抱歉, 此文章已結案並標記, 不得回應', log_level=log.INFO,\n                                exceptions_=exceptions.CantResponse()),\n        connect_core.TargetUnit('(E)繼續編輯 (W)強制寫入', log_level=log.INFO, response='W' + command.enter),\n        connect_core.TargetUnit('請選擇簽名檔', response=str(sign_file) + command.enter),\n        connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),\n        connect_core.TargetUnit('編輯文章', log_level=log.INFO,\n                                response=str(content) + command.enter + command.ctrl_x),\n        connect_core.TargetUnit('請問要引用原文嗎', log_level=log.DEBUG, response='Y' + command.enter),\n        connect_core.TargetUnit('採用原標題[Y/n]?', log_level=log.DEBUG, response='Y' + command.enter),\n        reply_target_unit,\n        connect_core.TargetUnit('已順利寄出，是否自存底稿', log_level=log.DEBUG, response='Y' + command.enter),\n    ]\n\n    api.connect_core.send(\n        cmd,\n        target_list,\n        screen_timeout=api.config.screen_long_timeout)\n\n    log.logger.info(i18n.success)\n"
  },
  {
    "path": "PyPtt/_api_search_user.py",
    "content": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\nfrom . import log\n\n\ndef search_user(api, ptt_id: str, min_page: int, max_page: int) -> list:\n    _api_util.one_thread(api)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    check_value.check_type(ptt_id, str, 'ptt_id')\n    if min_page is not None:\n        check_value.check_index('min_page', min_page)\n    if max_page is not None:\n        check_value.check_index('max_page', max_page)\n    if min_page is not None and max_page is not None:\n        check_value.check_index_range('min_page', min_page, 'max_page', max_page)\n\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append('T')\n    cmd_list.append(command.enter)\n    cmd_list.append('Q')\n    cmd_list.append(command.enter)\n    cmd_list.append(ptt_id)\n    cmd = ''.join(cmd_list)\n\n    if min_page is not None:\n        template = min_page\n    else:\n        template = 1\n\n    appendstr = ' ' * template\n    cmdtemp = cmd + appendstr\n\n    target_list = [\n        connect_core.TargetUnit('任意鍵', break_detect=True)]\n\n    resultlist = []\n\n    log.logger.info(i18n.search_user)\n\n    while True:\n\n        api.connect_core.send(\n            cmdtemp,\n            target_list)\n        ori_screen = api.connect_core.get_screen_queue()[-1]\n        # print(OriScreen)\n        # print(len(OriScreen.split('\\n')))\n\n        if len(ori_screen.split('\\n')) == 2:\n            result_id = ori_screen.split('\\n')[1]\n            result_id = result_id[result_id.find(' ') + 1:].strip()\n            # print(result_id)\n\n            resultlist.append(result_id)\n            break\n        else:\n\n            ori_screen = ori_screen.split('\\n')[3:-1]\n            ori_screen = '\\n'.join(ori_screen)\n\n            templist = ori_screen.replace('\\n', ' ')\n\n            while '  ' in templist:\n                templist = templist.replace('  ', ' ')\n\n            templist = templist.split(' ')\n            resultlist.extend(templist)\n\n            # print(templist)\n            # print(len(templist))\n\n            if len(templist) != 100 and len(templist) != 120:\n                break\n\n            template += 1\n            if max_page is not None:\n                if template > max_page:\n                    break\n\n            cmdtemp = ' '\n\n    api.connect_core.send(\n        command.enter,\n        [\n            # 《ＩＤ暱稱》\n            connect_core.TargetUnit('《ＩＤ暱稱》', response=command.enter),\n            connect_core.TargetUnit('查詢網友', break_detect=True)\n        ]\n    )\n    log.logger.info(i18n.success)\n\n    return list(filter(None, resultlist))\n"
  },
  {
    "path": "PyPtt/_api_set_board_title.py",
    "content": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import lib_util\n\n\ndef set_board_title(api, board: str, new_title: str) -> None:\n    # 第一支板主專用 api\n    _api_util.one_thread(api)\n\n    _api_util.goto_board(api, board)\n\n    if not api._is_login:\n        raise exceptions.RequireLogin(i18n.require_login)\n\n    if not api.is_registered_user:\n        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())\n\n    check_value.check_type(board, str, 'board')\n    check_value.check_type(new_title, str, 'new_title')\n\n    _api_util.check_board(\n        api,\n        board,\n        check_moderator=True)\n\n    cmd_list = []\n    cmd_list.append('I')\n    cmd_list.append(command.ctrl_p)\n    cmd_list.append('b')\n    cmd_list.append(command.enter)\n    cmd_list.append(command.backspace * 31)\n    cmd_list.append(new_title)\n    cmd_list.append(command.enter * 2)\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('◆ 已儲存新設定', break_detect=True),\n        connect_core.TargetUnit('◆ 未改變任何設定', break_detect=True),\n    ]\n\n    api.connect_core.send(\n        cmd,\n        target_list,\n        screen_timeout=api.config.screen_long_timeout)\n"
  },
  {
    "path": "PyPtt/_api_util.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport re\nimport threading\nfrom typing import Dict, Optional\n\nfrom . import _api_get_board_info\nfrom . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import log\nfrom . import screens\n\n\ndef get_content(api, post_mode: bool = True):\n    api.Unconfirmed = False\n\n    def is_unconfirmed_handler(screen):\n        api.Unconfirmed = True\n\n    if post_mode:\n        cmd = command.enter * 2\n    else:\n        cmd = command.enter\n\n    target_list = [\n        # 待證實文章\n        connect_core.TargetUnit('本篇文章內容經站方授權之板務管理人員判斷有尚待證實之處', response=' ',\n                                handler=is_unconfirmed_handler),\n        connect_core.TargetUnit(screens.Target.PostEnd, log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit(screens.Target.InPost, log_level=log.DEBUG, break_detect=True),\n        connect_core.TargetUnit(screens.Target.PostNoContent, log_level=log.DEBUG, break_detect=True),\n        # 動畫文章\n        connect_core.TargetUnit(screens.Target.Animation, response=command.go_main_menu_type_q,\n                                break_detect_after_send=True),\n    ]\n\n    line_from_pattern = re.compile('[\\d]+~[\\d]+')\n\n    has_control_code = False\n    control_code_mode = False\n    push_start = False\n    content_start_exist = False\n    content_start_jump = False\n    content_start_jump_set = False\n\n    first_page = True\n    origin_post = []\n    stop_dict = dict()\n\n    while True:\n        index = api.connect_core.send(cmd, target_list)\n        if index == 3 or index == 4:\n            return None, False\n\n        last_screen = api.connect_core.get_screen_queue()[-1]\n        lines = last_screen.split('\\n')\n        last_line = lines[-1]\n        lines.pop()\n        last_screen = '\\n'.join(lines)\n\n        if screens.Target.content_start in last_screen and not content_start_exist:\n            content_start_exist = True\n\n        if content_start_exist:\n            if not content_start_jump_set:\n                if screens.Target.content_start not in last_screen:\n                    content_start_jump = True\n                    content_start_jump_set = True\n            else:\n                content_start_jump = False\n\n        pattern_result = line_from_pattern.search(last_line)\n        if pattern_result is None:\n            control_code_mode = True\n            has_control_code = True\n        else:\n            last_read_line_list = pattern_result.group(0).split('~')\n            last_read_line_a_temp = int(last_read_line_list[0])\n            last_read_line_b_temp = int(last_read_line_list[1])\n            if control_code_mode:\n                last_read_line_a = last_read_line_a_temp - 1\n                last_read_line_b = last_read_line_b_temp - 1\n            control_code_mode = False\n\n        if first_page:\n            first_page = False\n            origin_post.append(last_screen)\n        else:\n            # 這裡是根據觀察畫面行數的變化歸納出的神奇公式...\n            # 輸出的結果是要判斷出畫面的最後 x 行是新的文章內容\n            #\n            # 這裡是 PyPtt 最黑暗最墮落的地方，所有你所知的程式碼守則，在這裡都不適用\n            # 每除完一次錯誤，我會陷入嚴重的創傷後壓力症候群，而我的腦袋會自動選擇遺忘這裡所有的一切\n            # 以確保下一個週一，我可以正常上班\n            # but it works!\n\n            # print(LastScreen)\n            # print(f'last_read_line_a_temp [{last_read_line_a_temp}]')\n            # print(f'last_read_line_b_temp [{last_read_line_b_temp}]')\n            # print(f'last_read_line_a {last_read_line_a}')\n            # print(f'last_read_line_b {last_read_line_b}')\n            # print(f'GetLineB {last_read_line_a_temp - last_read_line_a}')\n            # print(f'GetLineA {last_read_line_b_temp - last_read_line_b}')\n            # print(f'show line {last_read_line_b_temp - last_read_line_a_temp + 1}')\n            if not control_code_mode:\n\n                if last_read_line_a_temp in stop_dict:\n                    new_content_part = '\\n'.join(\n                        lines[-stop_dict[last_read_line_a_temp]:])\n                    stop_dict = dict()\n                else:\n                    get_line_b = last_read_line_b_temp - last_read_line_b\n                    if get_line_b > 0:\n                        # print('Type 1')\n                        new_content_part = '\\n'.join(lines[-get_line_b:])\n                        if index == 1 and len(new_content_part) == get_line_b - 1:\n                            new_content_part = '\\n'.join(lines[-(get_line_b * 2):])\n                        elif origin_post:\n                            last_line_temp = origin_post[-1].strip()\n                            try_line = lines[-(get_line_b + 1)].strip()\n\n                            if not last_line_temp.endswith(try_line):\n                                new_content_part = try_line + '\\n' + new_content_part\n                        stop_dict = dict()\n                    else:\n                        # 駐足現象，LastReadLineB跟上一次相比並沒有改變\n                        if (last_read_line_b_temp + 1) not in stop_dict:\n                            stop_dict[last_read_line_b_temp + 1] = 1\n                        stop_dict[last_read_line_b_temp + 1] += 1\n\n                        get_line_a = last_read_line_a_temp - last_read_line_a\n\n                        if get_line_a > 0:\n                            # print(f'Type 2 get_line_a [{get_line_a}]')\n                            new_content_part = '\\n'.join(lines[-get_line_a:])\n                        else:\n                            new_content_part = '\\n'.join(lines)\n\n            else:\n                new_content_part = lines[-1]\n\n            origin_post.append(new_content_part)\n\n            log.logger.debug('NewContentPart', new_content_part)\n\n        if index == 1:\n            if content_start_jump and len(new_content_part) == 0:\n                get_line_b += 1\n                new_content_part = '\\n'.join(lines[-get_line_b:])\n\n                origin_post.pop()\n                origin_post.append(new_content_part)\n            break\n\n        if not control_code_mode:\n            last_read_line_a = last_read_line_a_temp\n            last_read_line_b = last_read_line_b_temp\n\n        for EC in screens.Target.content_end_list:\n            if EC in last_screen:\n                push_start = True\n                break\n\n        if push_start:\n            cmd = command.right\n        else:\n            cmd = command.down\n\n    # print(api.Unconfirmed)\n    origin_post = '\\n'.join(origin_post)\n    # OriginPost = [line.strip() for line in OriginPost.split('\\n')]\n    # OriginPost = '\\n'.join(OriginPost)\n\n    log.logger.debug('OriginPost', origin_post)\n\n    return origin_post, has_control_code\n\n\nmail_capacity: Optional[tuple[int, int]] = None\n\n\ndef get_mailbox_capacity(api) -> tuple[int, int]:\n    global mail_capacity\n    if mail_capacity is not None:\n        return mail_capacity\n\n    last_screen = api.connect_core.get_screen_queue()[-1]\n    capacity_line = last_screen.split('\\n')[2]\n\n    log.logger.debug('capacity_line', capacity_line)\n\n    pattern_result = re.compile('(\\d+)/(\\d+)').search(capacity_line)\n    if pattern_result is not None:\n        current_capacity = int(pattern_result.group(0).split('/')[0])\n        max_capacity = int(pattern_result.group(0).split('/')[1])\n\n        log.logger.debug('current_capacity', current_capacity)\n        log.logger.debug('max_capacity', max_capacity)\n\n        mail_capacity = (current_capacity, max_capacity)\n        return current_capacity, max_capacity\n    return 0, 0\n\n\n# >     1   112/09 ericsk       □ [心得] 終於開板了\n# ┌── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─┐\n# │ 文章代碼(AID): #13cPSYOX (Python) [ptt.cc] [心得] 終於開板了  │\n# │ 文章網址: https://www.ptt.cc/bbs/Python/M.1134139170.A.621.html      │\n# │ 這一篇文章值 2 Ptt幣                                              │\n# └── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─┘\n\ndef parse_query_post(api, ori_screen):\n    lock_post = False\n    try:\n        cursor_line = [line for line in ori_screen.split(\n            '\\n') if line.strip().startswith(api.cursor)][0]\n    except Exception as e:\n        print(api.cursor)\n        print(ori_screen)\n        raise e\n\n    post_author = cursor_line\n    if '□' in post_author:\n        post_author = post_author[:post_author.find('□')].strip()\n    elif 'R:' in post_author:\n        post_author = post_author[:post_author.find('R:')].strip()\n    elif ' 轉 ' in post_author:\n        post_author = post_author[:post_author.find('轉')].strip()\n    elif ' 鎖 ' in post_author:\n        post_author = post_author[:post_author.find('鎖')].strip()\n        lock_post = True\n    post_author = post_author[post_author.rfind(' '):].strip()\n\n    post_title = cursor_line\n    if ' □ ' in post_title:\n        post_title = post_title[post_title.find('□') + 1:].strip()\n    elif ' R:' in post_title:\n        post_title = post_title[post_title.find('R:'):].strip()\n    elif ' 轉 ' in post_title:\n        post_title = post_title[post_title.find('轉') + 1:].strip()\n        post_title = f'Fw: {post_title}'\n    elif ' 鎖 ' in post_title:\n        post_title = post_title[post_title.find('鎖') + 1:].strip()\n\n    ori_screen_temp = ori_screen[ori_screen.find('┌──'):]\n    ori_screen_temp = ori_screen_temp[:ori_screen_temp.find('└──')]\n\n    aid_line = [line for line in ori_screen.split(\n        '\\n') if line.startswith('│ 文章代碼(AID)')]\n\n    post_aid = None\n    if len(aid_line) == 1:\n        aid_line = aid_line[0]\n        pattern = re.compile('#[\\w|-]+')\n        pattern_result = pattern.search(aid_line)\n        post_aid = pattern_result.group(0)[1:]\n\n    pattern = re.compile('文章網址: https:[\\S]+html')\n    pattern_result = pattern.search(ori_screen_temp)\n\n    if pattern_result is None:\n        post_web = None\n    else:\n        post_web = pattern_result.group(0)[6:]\n\n    pattern = re.compile('這一篇文章值 [\\d]+ Ptt幣')\n    pattern_result = pattern.search(ori_screen_temp)\n    if pattern_result is None:\n        # 特殊文章無價格\n        post_money = -1\n    else:\n        post_money = pattern_result.group(0)[7:]\n        post_money = post_money[:post_money.find(' ')]\n        post_money = int(post_money)\n\n    pattern = re.compile('[\\d]+\\/[\\d]+')\n    pattern_result = pattern.search(cursor_line)\n    if pattern_result is None:\n        list_date = None\n    else:\n        list_date = pattern_result.group(0)\n        list_date = list_date[-5:]\n    # print(list_date)\n\n    # >  7485   9 8/09 CodingMan    □ [閒聊] PTT Library 更新\n    # > 79189 M 1 9/17 LittleCalf   □ [公告] 禁言退文公告\n    # >781508 +爆 9/17 jodojeda     □ [新聞] 國人吃魚少 學者：應把吃魚當成輕鬆愉快\n    # >781406 +X1 9/17 kingofage111 R: [申請] ReDmango 請辭Gossiping板主職務\n\n    pattern = re.compile('[\\d]+')\n    pattern_result = pattern.search(cursor_line)\n    post_index = 0\n    if pattern_result is not None:\n        post_index = int(pattern_result.group(0))\n\n    push_number = cursor_line\n    push_number = push_number[7:11]\n    push_number = push_number.split(' ')\n    push_number = list(filter(None, push_number))\n\n    if len(push_number) == 0:\n        push_number = None\n    else:\n        push_number = push_number[-1]\n        if push_number.startswith('爆') or push_number.startswith('~爆'):\n            push_number = '爆'\n\n        if push_number.startswith('+') or push_number.startswith('~'):\n            push_number = push_number[1:]\n\n        if push_number.lower().startswith('m'):\n            push_number = push_number[1:]\n\n        if push_number.lower().startswith('!'):\n            push_number = push_number[1:]\n\n        if push_number.lower().startswith('s'):\n            push_number = push_number[1:]\n\n        if push_number.lower().startswith('='):\n            push_number = push_number[1:]\n\n        if len(push_number) == 0:\n            push_number = None\n\n    log.logger.debug('PostAuthor', post_author)\n    log.logger.debug('PostTitle', post_title)\n    log.logger.debug('PostAID', post_aid)\n    log.logger.debug('PostWeb', post_web)\n    log.logger.debug('PostMoney', post_money)\n    log.logger.debug('ListDate', list_date)\n    log.logger.debug('PushNumber', push_number)\n\n    return lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index\n\n\ndef get_search_condition_cmd(index_type: data_type.NewIndex, search_list: Optional[list] = None):\n    cmd_list = []\n    if not search_list:\n        return cmd_list\n\n    for search_type, search_condition in search_list:\n\n        if search_type == data_type.SearchType.KEYWORD:\n            cmd_list.append('/')\n        elif search_type == data_type.SearchType.AUTHOR:\n            cmd_list.append('a')\n        elif search_type == data_type.SearchType.MARK:\n            cmd_list.append('G')\n        elif index_type == data_type.NewIndex.BOARD:\n            if search_type == data_type.SearchType.COMMENT:\n                cmd_list.append('Z')\n            elif search_type == data_type.SearchType.MONEY:\n                cmd_list.append('A')\n            else:\n                continue\n        else:\n            continue\n\n        cmd_list.append(search_condition)\n        cmd_list.append(command.enter)\n\n    return cmd_list\n\n\ndef goto_board(api, board: str, refresh: bool = False, end: bool = False) -> None:\n    cmd_list = []\n    cmd_list.append(command.go_main_menu)\n    cmd_list.append('qs')\n    cmd_list.append(board)\n    cmd_list.append(command.enter)\n    cmd_list.append(command.space)\n\n    cmd = ''.join(cmd_list)\n\n    target_list = [\n        connect_core.TargetUnit('任意鍵', log_level=log.DEBUG, response=' '),\n        connect_core.TargetUnit('互動式動畫播放中', log_level=log.DEBUG, response=command.ctrl_c),\n        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),\n    ]\n\n    if refresh:\n        current_refresh = True\n    else:\n        if board.lower() in api._goto_board_list:\n            current_refresh = True\n        else:\n            current_refresh = False\n    api._goto_board_list.append(board.lower())\n    api.connect_core.send(cmd, target_list, refresh=current_refresh)\n\n    if end:\n        cmd_list = []\n        cmd_list.append('1')\n        cmd_list.append(command.enter)\n        cmd_list.append('$')\n        cmd = ''.join(cmd_list)\n\n        target_list = [\n            connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),\n        ]\n\n        api.connect_core.send(cmd, target_list)\n\n\ndef one_thread(api):\n    current_thread_id = threading.get_ident()\n    if current_thread_id != api._thread_id:\n        raise exceptions.MultiThreadOperated()\n\n\n@functools.lru_cache(maxsize=64)\ndef check_board(api, board: str, check_moderator: bool = False) -> Dict:\n    if board.lower() not in api._exist_board_list:\n        board_info = _api_get_board_info.get_board_info(api, board, get_post_kind=False, call_by_others=False)\n        api._exist_board_list.append(board.lower())\n        api._board_info_list[board.lower()] = board_info\n\n        moderators = board_info[data_type.BoardField.moderators]\n        moderators = [x.lower() for x in moderators]\n        api._moderators[board.lower()] = moderators\n        api._board_info_list[board.lower()] = board_info\n\n    if check_moderator:\n        if api.ptt_id.lower() not in api._moderators[board.lower()]:\n            raise exceptions.NeedModeratorPermission(board)\n\n    return api._board_info_list[board.lower()]\n"
  },
  {
    "path": "PyPtt/check_value.py",
    "content": "from . import i18n\nfrom . import log\n\n\ndef check_type(value, value_type, name) -> None:\n    if not isinstance(value, value_type):\n        if value_type is str:\n            raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_string}, but got {value}')\n        elif value_type is int:\n            raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_integer}, but got {value}')\n        elif value_type is bool:\n            raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_boolean}, but got {value}')\n        else:\n            raise TypeError(f'[PyPtt] {name} {i18n.must_be} {value_type}, but got {value}')\n\n\ndef check_range(value, min_value, max_value, name) -> None:\n    check_type(value, int, name)\n    check_type(min_value, int, 'min_value')\n    check_type(max_value, int, 'max_value')\n\n    if min_value <= value <= max_value:\n        return\n    raise ValueError(f'{name} {value} {i18n.must_between} {min_value} ~ {max_value}')\n\n\ndef check_index(name, index, max_value=None) -> None:\n    check_type(index, int, name)\n    if index < 1:\n        raise ValueError(f'{name} {i18n.must_bigger_than} 0')\n\n    if max_value is not None:\n        if index > max_value:\n            log.logger.info('index', index)\n            log.logger.info('max_value', max_value)\n            raise ValueError(f'{name} {index} {i18n.must_between} 0 ~ {max_value}')\n\n\ndef check_index_range(start_name, start_index, end_name, end_index, max_value=None) -> None:\n    check_type(start_index, int, start_name)\n    check_type(end_index, int, end_name)\n\n    if start_index < 1:\n        raise ValueError(f'{start_name} {start_index} {i18n.must_bigger_than} 0')\n\n    if end_index <= 1:\n        raise ValueError(f'{end_name} {end_index} {i18n.must_bigger_than} 1')\n\n    if start_index > end_index:\n        raise ValueError(f'{end_name} {end_index} {i18n.must_bigger_than} {start_name} {start_index}')\n\n    if max_value is not None:\n        if start_index > max_value:\n            raise ValueError(f'{start_name} {start_index} {i18n.must_small_than} {max_value}')\n\n        if end_index > max_value:\n            raise ValueError(f'{end_name} {end_index} {i18n.must_small_than} {max_value}')\n\n\nif __name__ == '__main__':\n    QQ = str\n\n    if QQ is str:\n        print('1')\n\n    if QQ == str:\n        print('2')\n\n    if isinstance('', QQ):\n        print('3')\n"
  },
  {
    "path": "PyPtt/command.py",
    "content": "# http://www.physics.udel.edu/~watson/scen103/ascii.html\nenter = '\\r'\ntab = '\\t'\nctrl_c = '\\x03'\nctrl_d = '\\x04'\nctrl_e = '\\x05'\nctrl_f = '\\x06'\nctrl_h = '\\x08'\nctrl_l = '\\x0C'\nctrl_p = '\\x10'\nctrl_u = '\\x15'\nctrl_x = '\\x18'\nctrl_y = '\\x19'\nctrl_z = '\\x1A'\nstar = '\\x2A'\nup = '\\x1b\\x4fA'\ndown = '\\x1b\\x4fB'\nright = '\\x1b\\x4fC'\nleft = '\\x1b\\x4fD'\n\nspace = ' '\nquery_post = 'Q'\ncomment = 'X'\ngo_main_menu = ' ' + left * 5\ngo_main_menu_type_q = 'q' * 5\nrefresh = ctrl_l\ncontrol_code = ctrl_u + star\nbackspace = ctrl_h\n"
  },
  {
    "path": "PyPtt/config.py",
    "content": "from . import data_type\nfrom . import log\n\n\nclass Config:\n    # retry_wait_time 秒後重新連線\n    retry_wait_time = 3\n\n    # ScreenLTimeOut 秒後判定此畫面沒有可辨識的目標\n    screen_timeout = 3.0\n\n    # screen_long_timeout 秒後判定此畫面沒有可辨識的目標\n    # 適用於需要特別等待的情況，例如: 剔除其他登入等等\n    # 建議不要低於 10 秒，剔除其他登入最長可能會花費約六到七秒\n    screen_long_timeout = 10.0\n\n    # screen_post_timeout 秒後判定此畫面沒有可辨識的目標\n    # 適用於貼文等待的情況，建議不要低於 60 秒\n    screen_post_timeout = 60.0\n\n    # 預設語言\n    language = data_type.Language.MANDARIN\n\n    # 預設 log 等級\n    log_level = log.INFO\n\n    # 預設不剔除其他登入\n    kick_other_session = False\n\n    # 預設登入 PTT1\n    host = data_type.HOST.PTT1\n\n    # 預設採用 websockets\n    connect_mode = None\n\n    # 預設使用 23\n    port = 23\n\n    logger_callback = None\n\n\nLOGGER_CONFIG = {\n\n}\n"
  },
  {
    "path": "PyPtt/connect_core.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport ssl\nimport telnetlib\nimport threading\nimport time\nimport traceback\nimport warnings\nfrom typing import Any\n\nimport websockets\nimport websockets.exceptions\nimport websockets.http\n\nimport PyPtt\nfrom . import command\nfrom . import data_type\nfrom . import exceptions\nfrom . import i18n\nfrom . import log\nfrom . import screens\n\nwebsockets.http.USER_AGENT += f' PyPtt/{PyPtt.__version__}'\n\nssl_context = ssl.create_default_context()\n\n\nclass TargetUnit:\n    def __init__(self, detect_target, log_level: log.LogLevel = None, response: [Any | str] = '', break_detect=False,\n                 break_detect_after_send=False, exceptions_=None, refresh=True, secret=False, handler=None,\n                 max_match: int = 0):\n\n        self.detect_target = detect_target\n        if log_level is None:\n            self.log_level = log.INFO\n        else:\n            self.log_level = log_level\n        self._response_func = response\n        self._break_detect = break_detect\n        self._exception = exceptions_\n        self._refresh = refresh\n        self._break_after_send = break_detect_after_send\n        self._secret = secret\n        self._Handler = handler\n        self._max_match = max_match\n        self._current_match = 0\n\n    def is_match(self, screen: str) -> bool:\n        if self._current_match >= self._max_match > 0:\n            return False\n        if isinstance(self.detect_target, str):\n            if self.detect_target in screen:\n                self._current_match += 1\n                return True\n            return False\n        elif isinstance(self.detect_target, list):\n            for Target in self.detect_target:\n                if Target not in screen:\n                    return False\n            self._current_match += 1\n            return True\n\n    def get_detect_target(self):\n        return self.detect_target\n\n    def get_log_level(self):\n        return self.log_level\n\n    def get_response(self, screen: str) -> str:\n        if callable(self._response_func):\n            return self._response_func(screen)\n        return self._response_func\n\n    def is_break(self) -> bool:\n        return self._break_detect\n\n    def raise_exception(self):\n        if isinstance(self._exception, Exception):\n            raise self._exception\n\n    def is_refresh(self) -> bool:\n        return self._refresh\n\n    def is_break_after_send(self) -> bool:\n        return self._break_after_send\n\n    def is_secret(self) -> bool:\n        return self._secret\n\n\nclass RecvData:\n    def __init__(self):\n        self.data = None\n\n\nasync def websocket_recv_func(core, recv_data_obj):\n    recv_data_obj.data = await core.recv()\n\n\nasync def websocket_receiver(core, screen_timeout, recv_data_obj):\n    # Wait for at most 1 second\n    await asyncio.wait_for(\n        websocket_recv_func(core, recv_data_obj),\n        timeout=screen_timeout)\n\n\nclass ReceiveDataQueue(object):\n    def __init__(self):\n        self._ReceiveDataQueue = []\n\n    def add(self, screen):\n        self._ReceiveDataQueue.append(screen)\n        self._ReceiveDataQueue = self._ReceiveDataQueue[-10:]\n\n    def get(self, last=1):\n        return self._ReceiveDataQueue[-last:]\n\n\nclass API(object):\n    def __init__(self, config):\n\n        self.current_encoding = 'big5uao'\n        self.config = config\n        self._RDQ = ReceiveDataQueue()\n        self._UseTooManyResources = TargetUnit(screens.Target.use_too_many_resources,\n                                               exceptions_=exceptions.UseTooManyResources())\n\n    def connect(self) -> None:\n        def _wait():\n            for i in range(self.config.retry_wait_time):\n\n                if self.config.host == data_type.HOST.PTT1:\n                    log.logger.info(i18n.prepare_connect_again, i18n.PTT, str(self.config.retry_wait_time - i))\n                elif self.config.host == data_type.HOST.PTT2:\n                    log.logger.info(i18n.prepare_connect_again, i18n.PTT2, str(self.config.retry_wait_time - i))\n                elif self.config.host == data_type.HOST.LOCALHOST:\n                    log.logger.info(i18n.prepare_connect_again, i18n.localhost, str(self.config.retry_wait_time - i))\n                else:\n                    log.logger.info(i18n.prepare_connect_again, self.config.host, str(self.config.retry_wait_time - i))\n\n                time.sleep(1)\n\n        warnings.filterwarnings(\"ignore\", category=DeprecationWarning)\n\n        self.current_encoding = 'big5uao'\n        # self.log.py.info(i18n.connect_core, i18n.active)\n\n        if self.config.host == data_type.HOST.PTT1:\n            telnet_host = 'ptt.cc'\n            websocket_host = 'wss://ws.ptt.cc/bbs/'\n            websocket_origin = 'https://term.ptt.cc'\n        elif self.config.host == data_type.HOST.PTT2:\n            telnet_host = 'ptt2.cc'\n            websocket_host = 'wss://ws.ptt2.cc/bbs/'\n            websocket_origin = 'https://term.ptt2.cc'\n        elif self.config.host == data_type.HOST.LOCALHOST:\n            telnet_host = 'localhost'\n            websocket_host = 'wss://localhost'\n            websocket_origin = 'https://term.ptt.cc'\n        else:\n            telnet_host = self.config.host\n            websocket_host = f'wss://{self.config.host}'\n            websocket_origin = 'https://term.ptt.cc'\n\n        connect_success = False\n\n        for _ in range(2):\n\n            try:\n                if self.config.connect_mode == data_type.ConnectMode.TELNET:\n                    self._core = telnetlib.Telnet(telnet_host, self.config.port)\n                else:\n                    if not threading.current_thread() is threading.main_thread():\n                        loop = asyncio.new_event_loop()\n                        asyncio.set_event_loop(loop)\n\n                    log.logger.debug('USER_AGENT', websockets.http.USER_AGENT)\n                    self._core = asyncio.get_event_loop().run_until_complete(\n                        websockets.connect(\n                            websocket_host,\n                            origin=websocket_origin,\n                            ssl=ssl_context))\n\n                connect_success = True\n            except Exception as e:\n                traceback.print_tb(e.__traceback__)\n                print(e)\n\n                if self.config.host == data_type.HOST.PTT1:\n                    log.logger.info(i18n.connect, i18n.PTT, i18n.fail)\n                elif self.config.host == data_type.HOST.PTT2:\n                    log.logger.info(i18n.connect, i18n.PTT2, i18n.fail)\n                elif self.config.host == data_type.HOST.LOCALHOST:\n                    log.logger.info(i18n.connect, i18n.localhost, i18n.fail)\n                else:\n                    log.logger.info(i18n.connect, self.config.host, i18n.fail)\n\n                _wait()\n                continue\n\n            break\n\n        if not connect_success:\n            raise exceptions.ConnectError(self.config)\n\n    def _decode_screen(self, receive_data_buffer, start_time, target_list, is_secret, refresh, msg):\n\n        break_detect_after_send = False\n        use_too_many_res = False\n\n        vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding)\n        screen = vt100_p.screen\n\n        find_target = False\n        target_index = -1\n        for target in target_list:\n            condition = target.is_match(screen)\n            if condition:\n                if target._Handler is not None:\n                    target._Handler(screen)\n                if len(screen) > 0:\n                    screens.show(self.config, screen)\n                    self._RDQ.add(screen)\n                    if target == self._UseTooManyResources:\n                        use_too_many_res = True\n                        # print(f'1 {use_too_many_res}')\n                        break\n                    target.raise_exception()\n\n                find_target = True\n\n                end_time = time.time()\n                log.logger.debug(i18n.spend_time, round(end_time - start_time, 3))\n\n                if target.is_break():\n                    target_index = target_list.index(target)\n                    break\n\n                msg = target.get_response(screen)\n\n                add_refresh = False\n                if target.is_refresh():\n                    add_refresh = True\n                elif refresh:\n                    add_refresh = True\n\n                if add_refresh:\n                    if not msg.endswith(command.refresh):\n                        msg = msg + command.refresh\n\n                is_secret = target.is_secret()\n\n                if target.is_break_after_send():\n                    # break_index = target_list.index(target)\n                    break_detect_after_send = True\n                break\n        return screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index\n\n    def send(self, msg: str, target_list: list, screen_timeout: int = 0, refresh: bool = True,\n             secret: bool = False) -> int:\n\n        if not all(isinstance(T, TargetUnit) for T in target_list):\n            raise ValueError('Item of TargetList must be TargetUnit')\n\n        if self._UseTooManyResources not in target_list:\n            target_list.append(self._UseTooManyResources)\n\n        if screen_timeout == 0:\n            current_screen_timeout = self.config.screen_timeout\n        else:\n            current_screen_timeout = screen_timeout\n\n        break_detect_after_send = False\n        is_secret = secret\n\n        use_too_many_res = False\n        while True:\n\n            if refresh and not msg.endswith(command.refresh):\n                msg = msg + command.refresh\n\n            try:\n                msg = msg.encode('utf-8', 'replace')\n            except AttributeError:\n                pass\n            except Exception as e:\n                traceback.print_tb(e.__traceback__)\n                print(e)\n                msg = msg.encode('utf-8', 'replace')\n\n            if is_secret:\n                log.logger.debug(i18n.send_msg, i18n.hide_sensitive_info)\n            else:\n                log.logger.debug(i18n.send_msg, str(msg))\n\n            if self.config.connect_mode == data_type.ConnectMode.TELNET:\n                try:\n                    self._core.read_very_eager()\n                    self._core.write(msg)\n                except EOFError:\n                    raise exceptions.ConnectionClosed()\n            else:\n                try:\n                    asyncio.get_event_loop().run_until_complete(\n                        self._core.send(msg))\n                except websockets.exceptions.ConnectionClosedError:\n                    raise exceptions.ConnectionClosed()\n                except RuntimeError:\n                    raise exceptions.ConnectionClosed()\n                except websockets.exceptions.ConnectionClosedOK:\n                    raise exceptions.ConnectionClosed()\n\n                if break_detect_after_send:\n                    return -1\n\n            msg = ''\n            receive_data_buffer = bytes()\n\n            start_time = time.time()\n            mid_time = time.time()\n            while mid_time - start_time < current_screen_timeout:\n\n                # print(1)\n                recv_data_obj = RecvData()\n\n                if self.config.connect_mode == data_type.ConnectMode.TELNET:\n                    try:\n                        recv_data_obj.data = self._core.read_very_eager()\n                    except EOFError:\n                        return -1\n\n                else:\n                    try:\n\n                        asyncio.get_event_loop().run_until_complete(\n                            websocket_receiver(\n                                self._core, current_screen_timeout, recv_data_obj))\n\n                    except websockets.exceptions.ConnectionClosed:\n                        if use_too_many_res:\n                            raise exceptions.UseTooManyResources()\n                        raise exceptions.ConnectionClosed()\n                    except websockets.exceptions.ConnectionClosedOK:\n                        raise exceptions.ConnectionClosed()\n                    except asyncio.TimeoutError:\n                        return -1\n                    except RuntimeError:\n                        raise exceptions.ConnectionClosed()\n\n                receive_data_buffer += recv_data_obj.data\n\n                screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \\\n                    self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)\n\n                if self.current_encoding == 'big5uao' and not find_target:\n                    self.current_encoding = 'utf-8'\n                    screen_, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \\\n                        self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)\n\n                    if find_target:\n                        screen = screen_\n                    else:\n                        self.current_encoding = 'big5uao'\n\n                # print(4)\n                if target_index != -1:\n                    return target_index\n\n                if use_too_many_res:\n                    continue\n\n                if find_target:\n                    break\n                if len(screen) > 0:\n                    screens.show(self.config, screen)\n                    self._RDQ.add(screen)\n\n                # print(6)\n\n                mid_time = time.time()\n\n            if not find_target:\n                return -1\n        return -2\n\n    def close(self):\n        if self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:\n            asyncio.get_event_loop().run_until_complete(self._core.close())\n        else:\n            self._core.close()\n\n    def get_screen_queue(self) -> list:\n        return self._RDQ.get(1)\n"
  },
  {
    "path": "PyPtt/data_type.py",
    "content": "import time\nfrom enum import auto\n\nfrom AutoStrEnum import AutoStrEnum\n\n\nclass Language:\n    MANDARIN = 'zh_TW'\n    ENGLISH = 'en_US'\n\n\nclass ConnectMode(AutoStrEnum):\n    TELNET = auto()\n    WEBSOCKETS = auto()\n\n\nclass SearchType(AutoStrEnum):\n    \"\"\"文章搜尋類型\"\"\"\n\n    NOPE = auto()\n    # 搜尋關鍵字    / ?\n    KEYWORD = auto()\n    # 搜尋作者      a\n    AUTHOR = auto()\n    # 搜尋推文數    Z\n    COMMENT = auto()\n    # 搜尋標記      G\n    MARK = auto()\n    # 搜尋稿酬      A\n    MONEY = auto()\n\n\nclass ReplyTo(AutoStrEnum):\n    # 回文類型\n\n    BOARD = auto()\n    MAIL = auto()\n    BOARD_MAIL = auto()\n\n\nclass CommentType(AutoStrEnum):\n    PUSH = auto()\n    BOO = auto()\n    ARROW = auto()\n\n\nclass UserField(AutoStrEnum):\n    ptt_id = auto()\n    money = auto()\n    login_count = auto()\n    account_verified = auto()\n    legal_post = auto()\n    illegal_post = auto()\n    activity = auto()\n    mail = auto()\n    last_login_date = auto()\n    last_login_ip = auto()\n    five_chess = auto()\n    chess = auto()\n    signature_file = auto()\n\n\nclass CommentField(AutoStrEnum):\n    type = auto()\n    author = auto()\n    content = auto()\n    ip = auto()\n    time = auto()\n\n\nclass PostStatus(AutoStrEnum):\n    EXISTS = auto()\n    DELETED_BY_AUTHOR = auto()\n    DELETED_BY_MODERATOR = auto()\n    DELETED_BY_UNKNOWN = auto()\n\n\nclass PostField(AutoStrEnum):\n    board = auto()\n    aid = auto()\n    index = auto()\n    author = auto()\n    date = auto()\n    title = auto()\n    content = auto()\n    money = auto()\n    url = auto()\n    ip = auto()\n    comments = auto()\n    post_status = auto()\n    list_date = auto()\n    has_control_code = auto()\n    pass_format_check = auto()\n    location = auto()\n    push_number = auto()\n    is_lock = auto()\n    full_content = auto()\n    is_unconfirmed = auto()\n\n\n# class WaterballInfo:\n#     def __init__(self, waterball_type, target, content, date):\n#         self.type: int = parse_para(int, waterball_type)\n#         self.target: str = parse_para(str, target)\n#         self.content: str = parse_para(str, content)\n#         self.date: str = parse_para(str, date)\n\n\nclass Cursor:\n    # 舊式游標\n    OLD: str = '●'\n    # 新式游標\n    NEW: str = '>'\n\n\nclass NewIndex(AutoStrEnum):\n    # 看板\n    BOARD = auto()\n    # 信箱\n    MAIL = auto()\n    # 網頁，尚不支援\n    # WEB = auto()\n\n\nclass HOST(AutoStrEnum):\n    # 批踢踢萬\n    PTT1 = auto()\n    # 批踢踢兔\n    PTT2 = auto()\n    # 本機測試用\n    LOCALHOST = auto()\n\n\nclass MarkType(AutoStrEnum):\n    # s 文章\n    S = auto()\n    # 標記文章\n    D = auto()\n    # 刪除標記文章\n    DELETE_D = auto()\n    # M 起來\n    M = auto()\n    # 待證實文章\n    UNCONFIRMED = auto()\n\n\nclass FavouriteBoardField(AutoStrEnum):\n    board = auto()\n    type = auto()\n    title = auto()\n\n\nclass MailField(AutoStrEnum):\n    origin_mail = auto()\n    author = auto()\n    title = auto()\n    date = auto()\n    content = auto()\n    ip = auto()\n    location = auto()\n    is_red_envelope = auto()\n\n\nclass BoardField(AutoStrEnum):\n    board = auto()\n    online_user = auto()\n    mandarin_des = auto()\n    moderators = auto()\n    open_status = auto()\n    into_top_ten_when_hide = auto()\n    can_non_board_members_post = auto()\n    can_reply_post = auto()\n    self_del_post = auto()\n    can_comment_post = auto()\n    can_boo_post = auto()\n    can_fast_push = auto()\n    min_interval_between_comments = auto()\n    is_comment_record_ip = auto()\n    is_comment_aligned = auto()\n    can_moderators_del_illegal_content = auto()\n    does_tran_post_auto_recorded_and_require_post_permissions = auto()\n    is_cool_mode = auto()\n    is_require18 = auto()\n    require_login_time = auto()\n    require_illegal_post = auto()\n    # post_kind = auto()\n    post_kind_list = auto()\n\n\nclass Compare(AutoStrEnum):\n    BIGGER = auto()\n    SAME = auto()\n    SMALLER = auto()\n    UNKNOWN = auto()\n\n\nclass TimedDict:\n    def __init__(self, timeout: int = 0):\n        self.timeout = timeout\n        self.data = {}\n        self.timestamps = {}\n\n    def __setitem__(self, key, value):\n        self.data[key] = value\n        self.timestamps[key] = time.time()\n\n    def __getitem__(self, key):\n        if key not in self.data:\n            raise KeyError(key)\n        timestamp = self.timestamps[key]\n        if time.time() - timestamp > self.timeout > 0:\n            del self.data[key]\n            del self.timestamps[key]\n            raise KeyError(key)\n        return self.data[key]\n\n    def __contains__(self, key):\n        try:\n            self[key]\n        except KeyError:\n            return False\n        else:\n            return True\n\n    def __len__(self):\n        self.cleanup()\n        return len(self.data)\n\n    def cleanup(self):\n        if self.timeout == 0:\n            return\n\n        now = time.time()\n        to_remove = [key for key, timestamp in self.timestamps.items()\n                     if now - timestamp > self.timeout > 0]\n        for key in to_remove:\n            del self.data[key]\n            del self.timestamps[key]\n"
  },
  {
    "path": "PyPtt/exceptions.py",
    "content": "from . import data_type\nfrom . import i18n\n\n\nclass Error(Exception):\n    pass\n\n\nclass UnknownError(Error):\n    def __init__(self, message):\n        self.message = message\n\n    def __str__(self):\n        return self.message\n\n\nclass RequireLogin(Error):\n    def __init__(self, message):\n        self.message = message\n\n    def __str__(self):\n        return self.message\n\n\nclass NoPermission(Error):\n    def __init__(self, message):\n        self.message = message\n\n    def __str__(self):\n        return self.message\n\n\nclass LoginError(Error):\n    def __init__(self):\n        self.message = i18n.login_fail\n\n    def __str__(self):\n        return self.message\n\n\nclass NoFastComment(Error):\n    def __init__(self):\n        self.message = i18n.no_fast_comment\n\n    def __str__(self):\n        return self.message\n\n\nclass NoSuchUser(Error):\n    def __init__(self, user):\n        self.message = i18n.no_such_user + ': ' + user\n\n    def __str__(self):\n        return self.message\n\n\nclass NoSuchMail(Error):\n    def __init__(self):\n        self.message = i18n.no_such_mail\n\n    def __str__(self):\n        return self.message\n\n\n# class UserOffline(Error):\n#     def __init__(self, user):\n#         self.message = i18n.user_offline + ': ' + user\n#\n#     def __str__(self):\n#         return self.message\n\n\n# class ParseError(Error):\n#     def __init__(self, screen):\n#         self.message = screen\n#\n#     def __str__(self):\n#         return self.message\n\n\nclass NoMoney(Error):\n    def __init__(self):\n        self.message = i18n.no_money\n\n    def __str__(self):\n        return self.message\n\n\nclass NoSuchBoard(Error):\n    def __init__(self, config, board):\n        if config.host == data_type.HOST.PTT1:\n            self.message = [\n                i18n.PTT,\n                i18n.no_such_board\n            ]\n        else:\n            self.message = [\n                i18n.PTT2,\n                i18n.no_such_board\n            ]\n\n        if config.language == data_type.Language.MANDARIN:\n            self.message = ''.join(self.message) + ': ' + board\n        else:\n            self.message = ' '.join(self.message) + ': ' + board\n\n    def __str__(self):\n        return self.message\n\n\nclass ConnectionClosed(Error):\n    def __init__(self):\n        self.message = i18n.connection_closed\n\n    def __str__(self):\n        return self.message\n\n\nclass UnregisteredUser(Error):\n    def __init__(self, api_name):\n        self.message = i18n.unregistered_user_cant_use_this_api + ': ' + api_name\n\n    def __str__(self):\n        return self.message\n\n\nclass MultiThreadOperated(Error):\n    def __init__(self):\n        self.message = i18n.multi_thread_operate\n\n    def __str__(self):\n        return self.message\n\n\nclass WrongIDorPassword(Error):\n    def __init__(self):\n        self.message = i18n.wrong_id_pw\n\n    def __str__(self):\n        return self.message\n\n\nclass WrongPassword(Error):\n    def __init__(self):\n        self.message = i18n.error_pw\n\n    def __str__(self):\n        return self.message\n\n\nclass LoginTooOften(Error):\n    def __init__(self):\n        self.message = i18n.login_too_often\n\n    def __str__(self):\n        return self.message\n\n\nclass UseTooManyResources(Error):\n    def __init__(self):\n        self.message = i18n.use_too_many_resources\n\n    def __str__(self):\n        return self.message\n\n\nclass HostNotSupport(Error):\n    def __init__(self, api):\n        self.message = f'{i18n.ptt2_not_support}: {api}'\n\n    def __str__(self):\n        return self.message\n\n\nclass CantComment(Error):\n    def __init__(self):\n        self.message = i18n.no_comment\n\n    def __str__(self):\n        return self.message\n\n\nclass CantResponse(Error):\n    def __init__(self):\n        self.message = i18n.no_response\n\n    def __str__(self):\n        return self.message\n\n\nclass NeedModeratorPermission(Error):\n    def __init__(self, board):\n        self.message = f'{i18n.need_moderator_permission}: {board}'\n\n    def __str__(self):\n        return self.message\n\n\nclass ConnectError(Error):\n    def __init__(self, config):\n        self.message = i18n.connect_fail\n\n    def __str__(self):\n        return self.message\n\n\nclass NoSuchPost(Error):\n    def __init__(self, board, aid):\n        self.message = i18n.replace(\n            i18n.no_such_post,\n            board,\n            aid)\n\n    def __str__(self):\n        return self.message\n\n\nclass CanNotUseSearchPostCode(Error):\n    \"\"\"\n    此狀態下無法使用搜尋文章代碼(AID)功能\n    \"\"\"\n\n    def __init__(self):\n        self.message = i18n.can_not_use_search_post_code_f\n\n    def __str__(self):\n        return self.message\n\n\nclass UserHasPreviouslyBeenBanned(Error):\n    def __init__(self):\n        self.message = i18n.user_has_previously_been_banned\n\n    def __str__(self):\n        return self.message\n\n\nclass MailboxFull(Error):\n    def __init__(self):\n        self.message = i18n.mail_box_full\n\n    def __str__(self):\n        return self.message\n\n\nclass NoSearchResult(Error):\n    def __init__(self):\n        self.message = i18n.no_search_result\n\n    def __str__(self):\n        return self.message\n\n\n# 此帳號已設定為只能使用安全連線\n\nclass OnlySecureConnection(Error):\n    def __init__(self):\n        self.message = i18n.only_secure_connection\n\n    def __str__(self):\n        return self.message\n\n\nclass SetContactMailFirst(Error):\n    def __init__(self):\n        self.message = i18n.set_contact_mail_first\n\n    def __str__(self):\n        return self.message\n\n\nclass ResetYourContactEmail(Error):\n    def __init__(self):\n        self.message = i18n.reset_your_contact_email\n\n    def __str__(self):\n        return self.message\n"
  },
  {
    "path": "PyPtt/i18n.py",
    "content": "import os\nimport random\n\nimport yaml\n\nfrom . import __version__\nfrom . import data_type\n\nlocale_pool = {\n    data_type.Language.ENGLISH,\n    data_type.Language.MANDARIN\n}\n\n_script_path = os.path.dirname(os.path.abspath(__file__))\n_lang_data = {}\n\nmapping = {\n    '{version}': __version__,\n}\n\n\ndef replace(string, *args):\n    for i in range(len(args)):\n        target = f'{args[i]}'\n        string = string.replace(f'_target{i}_', target)\n    return string\n\n\ndef init(locale: str, cache: bool = False) -> None:\n    if locale not in locale_pool:\n        raise ValueError(f'Unknown locale: {locale}')\n\n    language_file = f'{_script_path}/lang/{locale}.yaml'\n    if not os.path.exists(language_file):\n        raise ValueError(f'Unknown locale file: {language_file}')\n\n    with open(language_file, \"r\") as f:\n        string_data = yaml.safe_load(f)\n\n    for k, v in string_data.items():\n\n        if isinstance(v, list):\n            v = random.choice(v)\n        elif isinstance(v, str):\n            pass\n        else:\n            raise ValueError(f'Unknown string data type: {v}')\n\n        if locale == data_type.Language.ENGLISH:\n            v = v[0].upper() + v[1:]\n\n        for mk, mv in mapping.items():\n            v = v.replace(mk, mv)\n\n        globals()[k] = v\n        if cache:\n            global _lang_data\n            _lang_data[k] = v\n"
  },
  {
    "path": "PyPtt/lang/en_US.yaml",
    "content": "PTT: PTT\nPTT2: PTT2\nactive: Active\nauthor: Author\nboard: Board\ncan_not_use_search_post_code_f: This status can not use the search PostField code function\ncatch_bottom_post_success: Catch bottom post success\nchange_pw: Change password\ncomment: Comment\ncomment_content: Comment content\ncomment_date: Comment date\ncomment_id: Comment id\nconnect: Connect\nconnect_core: Connect core\nconnect_fail: Connect fail\nconnect_mode_TELNET: Telnet\nconnect_mode_WEBSOCKET: WebSocket\nconnection_closed: Connection Closed\ncontent: Content\ncurrent_version: Current version\ndate: Date\ndelete_post: Delete post\ndevelopment_version: Running development version\ndone: Done\nenglish_module: English\nerror_pw: Wrong password\nfail: Fail\nfind_newest_index: Find newest index\nget_board_info: Get board info _target0_\nget_favourite_board_list: Query favourite board list\nget_mail: Get mail\ngive_money_to: give _target0_ _target1_ P coins\ngoodbye:\n  - good bye\n  - bye\n  - see you\n  - catch you later\n  - I hate to run, but...\n  - Until we meet again, I will wait\nhas_comment_permission: User has permission to comment\nhas_new_mail_goto_main_menu: New mail! Back to main menu\nhas_post_permission: Have permission to post\nhide_sensitive_info: Hide sensitive info\nin_login_process_please_wait: In login process, please wait\ninitialization: Init\nkick_other_login: Kick other login\nlatest_version: Running the latest version\nlocalhost: Localhost\nlogin_fail: Login fail\nlogin_id: Login id\nlogin_success: Login ... success\nlogin_too_often: Login too often\nlogout: Logout\nmail_box_full: Mail box is full\nmandarin_module: Mandarin\nmulti_thread_operate: Do not use a multi-thread to operate a PyPtt object\nmust_be: Must be\nmust_be_a_boolean: Must be a boolean\nmust_be_a_integer: Must be a integer\nmust_be_a_string: Must be a string\nmust_between: Must between\nmust_bigger_than: Must bigger than\nmust_small_than: Must smaller than\nneed_moderator_permission: Need moderator permission\nnew_cursor: New cursor\nnew_version: There is a new version\nno_comment: No comment\nno_fast_comment: No fast comment\nno_money: Not enough PTT coins\nno_permission: User Has No Permission\nno_response: This post has been closed and marked, no response\nno_search_result: No Search Result\nno_such_board: No such board\nno_such_mail: No such mail\nno_such_post: In _target0_, the post code is not EXISTS _target1_\nno_such_user: No such user\nnot_kick_other_login: Not kick other login\nnot_push_aligned: No push aligned\nnot_record_ip: Not record IP\nold_cursor: Old cursor\nonly_secure_connection: Skip registration form\npicks_in_register: Registration application processing order\npost: Post article\npost_deleted: Post has been deleted\nprepare_connect_again: Prepare connect again\nptt2_not_support: PTT2 not support this api\npush_aligned: Push aligned\nrecord_ip: Record ip\nreply_board: Respond to the BoardField\nreply_board_mail: Respond to the board and the mailbox of author\nreply_mail: Respond to the mailbox of author\nrequire_login: Please login first\nreset_your_contact_email: Please reset your contact email\nretry: Retry\nsearch_user: Search user\nsend_mail: Send mail\nsend_msg: Send msg\nset_connect_host: Set up the connect host\nset_connect_mode: Set up the connect mode\nset_contact_mail_first: Password can only be changed after setting the contact mailbox\nset_up_lang_module: Set up language module\nspend_time: Spend time\nsubstandard_post: Substandard post\nsuccess: Success\ntitle: Title\ntransaction_cancelled: The transaction is cancelled!\nunregistered_user_cant_use_all_api: Unregistered UserField Can't Use All API\nunregistered_user_cant_use_this_api: Unregistered UserField Can't Use This API\nupdate_remote_version: Fetching latest version\nuse_mailbox_api_will_logout_after_execution: If you use mailbox related functions, you will be logged out automatically after execution\nuse_too_many_resources: Use too many resources\nuser_has_previously_been_banned: User has previously been banned\nuser_offline: User offline\nwait_for_no_fast_comment: Because no fast comment, wait 5 sec\nwelcome: PyPtt v _target0_ developed by CodingMan\nwrong_id_pw: Wrong id or pw\n"
  },
  {
    "path": "PyPtt/lang/zh_TW.yaml",
    "content": "PTT: 批踢踢\nPTT2: 批踢踢兔\nactive: 啟動\nauthor: 作者\nboard: 看板\ncan_not_use_search_post_code_f: 此狀態下無法使用搜尋文章代碼(AID)功能\ncatch_bottom_post: 取得置底文章\nchange_pw: 變更密碼\ncomment: 推文\ncomment_content: 推文內文\ncomment_date: 推文日期\ncomment_id: 推文帳號\nconnect: 連線\nconnect_core: 連線核心\nconnect_fail: 連線失敗\nconnect_mode_TELNET: Telnet\nconnect_mode_WEBSOCKET: WebSocket\nconnection_closed: 連線已經被關閉\ncontent: 內文\ncurrent_version: 目前版本\ndate: 日期\ndelete_post: 刪除文章\ndevelopment_version: 正在執行開發版本\ndone: 完成\nenglish_module: 英文\nerror_pw: 密碼不正確\nfail: 失敗\nfind_newest_index: 找到最新編號\nget_board_info: 取得看板資訊 _target0_\nget_favourite_board_list: 取得我的最愛\nget_mail: 取得信件\ngive_money_to: 給 _target0_ _target1_ P 幣\ngoodbye:\n  - 再見\n  - 下次再見\n  - 再會\n  - 祝平安\n  - 謝謝你，我很開心\n  - 我們會再見面的\nhas_comment_permission: 確認擁有推文權限\nhas_new_mail_goto_main_menu: 有新信，回到主選單\nhas_post_permission: 確認擁有貼文權限\nhide_sensitive_info: 隱藏敏感資訊\nin_login_process_please_wait: 登入中，請稍候\ninitialization: 初始化\nkick_other_login: 強制執行剔除其他登入\nlatest_version: 正在執行最新版本\nlocalhost: 本機\nlogin_fail: 登入失敗\nlogin_id: 登入帳號\nlogin_success: 登入 ... 成功\nlogin_too_often: 登入太頻繁\nlogout: 登出\nmail_box_full: 郵件已滿\nmandarin_module: 繁體中文\nmulti_thread_operate: 請勿使用多線程同時操作一個 PyPtt 物件\nmust_be: 必須為\nmust_be_a_boolean: 必須為布林值\nmust_be_a_integer: 必須為數字\nmust_be_a_string: 必須為字串\nmust_between: 必須介於\nmust_bigger_than: 必須大於\nmust_small_than: 必須小於\nneed_moderator_permission: 需要板主權限\nnew_cursor: 新式游標\nnew_version: 有新版本\nno_comment: 禁止推薦\nno_fast_comment: 禁止快速連續推文\nno_money: PTT 幣不足\nno_permission: 使用者沒有權限\nno_response: 很抱歉, 此文章已結案並標記, 不得回應\nno_search_result: 沒有搜尋結果\nno_such_board: 無該看板\nno_such_mail: 無此信件\nno_such_post: _target0_ 板找不到這個文章代碼 _target1_\nno_such_user: 無該使用者\nnot_kick_other_login: 不剔除其他登入\nnot_push_aligned: 無推文對齊\nnot_record_ip: 不紀錄 IP\nold_cursor: 舊式游標\nonly_secure_connection: 跳過填寫註冊單\npicks_in_register: 註冊申請單處理順位\npost: 發佈文章\npost_deleted: 文章已經被刪除\nprepare_connect_again: 準備再次連線\nptt2_not_support: 批踢踢兔不支援此功能\npush_aligned: 推文對齊\nrecord_ip: 紀錄 IP\nreply_board: 回應至看板\nreply_board_mail: 回應至看板與作者信箱\nreply_mail: 回應至作者信箱\nrequire_login: 請先登入\nreset_your_contact_email: 請重新設定您的聯絡信箱\nretry: 重試\nsearch_user: 搜尋使用者\nsend_mail: 寄信\nsend_msg: 送出訊息\nset_connect_host: 設定連線主機\nset_connect_mode: 設定連線模式\nset_contact_mail_first: 請先設定聯絡信箱後才能修改密碼\nset_up_lang_module: 設定語言模組\nspend_time: 花費時間\nsubstandard_post: 不合規範文章\nsuccess: 成功\ntitle: 標題\ntransaction_cancelled: 交易取消!\nunregistered_user_cant_use_all_api: 未註冊使用者，將無法使用全部功能\nunregistered_user_cant_use_this_api: 未註冊使用者，無法使用此功能\nupdate_remote_version: 確認最新版本\nuse_mailbox_api_will_logout_after_execution: 如果使用信箱相關功能，將執行後自動登出\nuse_too_many_resources: 耗用過多資源\nuser_has_previously_been_banned: 使用者之前已被禁言\nuser_offline: 使用者離線\nwait_for_no_fast_comment: 因禁止快速連續推文，所以等待五秒\nwelcome: PyPtt v _target0_ 由 CodingMan 開發\nwrong_id_pw: 帳號密碼錯誤\n"
  },
  {
    "path": "PyPtt/lib_util.py",
    "content": "import functools\nimport os\nimport random\nimport re\nimport string\nimport time\nimport traceback\nfrom typing import Tuple\n\nimport requests\n\nfrom . import __version__\nfrom . import check_value\nfrom . import data_type\nfrom . import i18n\nfrom . import log\n\n\ndef get_file_name(path_str: str) -> str:\n    result = os.path.basename(path_str)\n    result = result[:result.find('.')]\n    return result\n\n\ndef get_current_func_name() -> str:\n    return traceback.extract_stack(None, 2)[0][2]\n\n\ndef findnth(haystack, needle, n) -> int:\n    parts = haystack.split(needle, n + 1)\n    if len(parts) <= n + 1:\n        return -1\n    return len(haystack) - len(parts[-1]) - len(needle)\n\n\ndef get_random_str(length) -> str:\n    return ''.join(random.choices(string.hexdigits, k=length))\n\n\n# 演算法參考 https://www.ptt.cc/man/C_Chat/DE98/DFF5/DB61/M.1419434423.A.DF0.html\n# aid 字元表\naid_table = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'\n\n\ndef get_aid_from_url(url: str) -> Tuple[str, str]:\n    # 檢查是否為字串\n    check_value.check_type(url, str, 'url')\n\n    # 檢查是否符合 PTT BOARD 文章網址格式\n    pattern = re.compile('https://www.ptt.cc/bbs/[-.\\w]+/M.[\\d]+.A[.\\w]*.html')\n    r = pattern.search(url)\n    if r is None:\n        raise ValueError('wrong parameter url must be www.ptt.cc post url')\n\n    board = url[23:]\n    board = board[:board.find('/')]\n\n    temp = url[url.rfind('/') + 1:].split('.')\n    # print(temp)\n\n    id_0 = int(temp[1])  # dec\n\n    aid_0 = ''\n    for _ in range(6):\n        index = id_0 % 64\n        aid_0 = f'{aid_table[index]}{aid_0}'\n        id_0 = int(id_0 / 64)\n\n    if temp[3] != 'html':\n        id_1 = int(temp[3], 16)  # hex\n        aid_1 = ''\n        for _ in range(2):\n            index = id_1 % 64\n            aid_1 = f'{aid_table[index]}{aid_1}'\n            id_1 = int(id_1 / 64)\n    else:\n        aid_1 = '00'\n\n    aid = f'{aid_0}{aid_1}'\n\n    return board, aid\n\n\nsync_version_compare: data_type.Compare = data_type.Compare.UNKNOWN\nsync_version_result: str = ''\n\n\ndef sync_version() -> Tuple[data_type.Compare, str]:\n    global sync_version_compare\n    global sync_version_result\n\n    if sync_version_compare is not data_type.Compare.UNKNOWN:\n        return sync_version_compare, sync_version_result\n\n    log.logger.info(i18n.update_remote_version)\n\n    r = None\n    for i in range(3):\n        try:\n            r = requests.get(\n                'https://raw.githubusercontent.com/PyPtt/PyPtt/master/PyPtt/__init__.py',\n                timeout=3)\n            break\n        except requests.exceptions.ReadTimeout:\n            log.logger.info(i18n.retry)\n            time.sleep(0.5)\n\n    if r is None:\n        log.logger.info(i18n.update_remote_version, i18n.fail)\n        return data_type.Compare.SAME, ''\n\n    log.logger.info(i18n.update_remote_version, i18n.success)\n\n    text = r.text\n\n    remote_version = [line for line in text.split('\\n') if line.startswith('__version__')][0]\n    remote_version = remote_version[remote_version.find(\"'\") + 1:]\n    remote_version = remote_version[:remote_version.find(\"'\")]\n\n    current_version = __version__\n    if 'dev' in current_version:\n        current_version = current_version[:current_version.find('dev') - 1]\n\n    version_list = [int(v) for v in current_version.split('.')]\n    remote_version_list = [int(v) for v in remote_version.split('.')]\n\n    sync_version_compare = data_type.Compare.SAME\n    for i in range(len(version_list)):\n        if remote_version_list[i] < version_list[i]:\n            sync_version_compare = data_type.Compare.BIGGER\n            break\n        if version_list[i] < remote_version_list[i]:\n            sync_version_compare = data_type.Compare.SMALLER\n            break\n    return sync_version_compare, remote_version\n\n\ndef uniform_new_line(text: str) -> str:\n    random_tag = get_random_str(10)\n\n    text = text.replace('\\r\\n', random_tag)\n    text = text.replace('\\n', '\\r\\n')\n    text = text.replace(random_tag, '\\r\\n')\n\n    return text\n\n\n@functools.lru_cache(maxsize=64)\ndef check_aid(aid: str) -> str:\n    if aid is None:\n        raise ValueError('aid is None')\n\n    if not isinstance(aid, str):\n        raise TypeError('aid is not str')\n\n    if aid.startswith('#'):\n        aid = aid[1:]\n\n    if len(aid) != 8:\n        raise ValueError('aid is not valid')\n\n    # check the char of aid is in aid_table or not\n    for char in aid:\n        if char not in aid_table:\n            raise ValueError('aid is not valid')\n\n    return f'#{aid}'\n\n\nif __name__ == '__main__':\n    check_aid('#1aBzRW4z')\n"
  },
  {
    "path": "PyPtt/log.py",
    "content": "import logging\nfrom typing import Optional\n\n\nclass LogLv:\n    _level: int\n\n    def __init__(self, level):\n        self._level = level\n\n    @property\n    def level(self):\n        return self._level\n\n    def __eq__(self, other):\n        return self.level == other.level\n\n\nSILENT = LogLv(logging.NOTSET)\nINFO = LogLv(logging.INFO)\nDEBUG = LogLv(logging.DEBUG)\n# deprecated use DEBUG instead\nTRACE = DEBUG\n\n\nclass LogLevel:\n    SILENT = LogLv(logging.NOTSET)\n    INFO = LogLv(logging.INFO)\n    DEBUG = LogLv(logging.DEBUG)\n    TRACE = DEBUG\n\n\n_logger_pool = {}\n\n_console_handler = logging.StreamHandler()\n_console_handler.setFormatter(logging.Formatter(\n    fmt='[%(asctime)s][%(name)s][%(levelname)s] %(message)s',\n    datefmt='%m.%d %H:%M:%S'))\n\n\ndef _combine_msg(*args) -> str:\n    \"\"\"\n    將多個字串組合成一個字串。\n\n    Args:\n        args: 要組合的字串。\n\n    Returns:\n        組合後的字串。\n    \"\"\"\n\n    if not args:\n        return ''\n\n    msg = list(map(str, args))\n    msg[0] = msg[0][0].upper() + msg[0][1:]\n\n    return ' '.join(msg)\n\n\nclass Logger:\n    logger: logging.Logger\n\n    def __init__(self, name: str, level: int = logging.NOTSET, logger_callback: Optional[callable] = None):\n\n        self.logger = logging.getLogger(name)\n        self.logger.setLevel(level)\n\n        if self.logger.hasHandlers():\n            for handler in self.logger.handlers:\n                handler.setFormatter(_console_handler.formatter)\n        else:\n            self.logger.addHandler(_console_handler)\n\n        self.logger_callback: Optional[callable] = None\n        if logger_callback and callable(logger_callback):\n            self.logger_callback = logger_callback\n\n    def info(self, *args):\n\n        if not self.logger.isEnabledFor(logging.INFO):\n            return\n\n        msg = _combine_msg(*args)\n        self.logger.info(msg)\n        if self.logger_callback:\n            self.logger_callback(msg)\n\n    def debug(self, *args):\n\n        if not self.logger.isEnabledFor(logging.DEBUG):\n            return\n\n        msg = _combine_msg(*args)\n        self.logger.debug(msg)\n        if self.logger_callback:\n            self.logger_callback(msg)\n\n\nlogger: Optional[Logger] = None\n\n\ndef init(log_level: LogLv, name: Optional[str] = None, logger_callback: Optional[callable] = None) -> Logger:\n    name = name or 'PyPtt'\n    current_logger = Logger(name, level=log_level.level, logger_callback=logger_callback)\n\n    if name == 'PyPtt':\n        global logger\n        logger = current_logger\n    return current_logger\n\n\nif __name__ == '__main__':\n    logger = init(INFO)\n\n    logger.info('1')\n    logger.info('1', '2')\n    logger.info('1', '2', '3')\n\n    logger.debug('debug 1')\n    logger.debug('1', '2')\n    logger.debug('1', '2', '3')\n\n    logger = init(DEBUG)\n\n    logger.info('1')\n    logger.info('1', '2')\n    logger.info('1', '2', '3')\n\n    logger.debug('debug 2')\n    logger.debug('1', '2')\n    logger.debug('1', '2', '3')\n"
  },
  {
    "path": "PyPtt/screens.py",
    "content": "import re\nimport sys\n\nfrom uao import register_uao\n\nfrom . import log\n\nregister_uao()\n\n\nclass Target:\n    MainMenu = [\n        '離開，再見',\n        '人, 我是',\n        '[呼叫器]',\n    ]\n\n    MainMenu_Exiting = [\n        '【主功能表】',\n        '您確定要離開',\n    ]\n\n    PTT1_QueryPost = [\n        '請按任意鍵繼續',\n        '文章代碼(AID):',\n        '文章網址:'\n    ]\n\n    PTT2_QueryPost = [\n        '請按任意鍵繼續',\n        '文章代碼(AID):'\n    ]\n\n    InBoard = [\n        '看板資訊/設定',\n        '文章選讀',\n        '相關主題'\n    ]\n\n    InBoardWithCursor = [\n        '【',\n        '看板資訊/設定',\n    ]\n    InBoardWithCursorLen = len(InBoardWithCursor)\n\n    # (h)說明 (←/q)離開\n    # (y)回應(X%)推文(h)說明(←)離開\n    # (y)回應(X/%)推文 (←)離開\n\n    InPost = [\n        '瀏覽',\n        '頁',\n        ')離開'\n    ]\n\n    PostEnd = [\n        '瀏覽',\n        '頁 (100%)',\n        ')離開'\n    ]\n\n    InWaterBallList = [\n        '瀏覽',\n        '頁',\n        '說明',\n    ]\n\n    WaterBallListEnd = [\n        '瀏覽',\n        '頁 (100%)',\n        '說明'\n    ]\n\n    PostIP_New = [\n        '※ 發信站: 批踢踢實業坊(ptt.cc), 來自:'\n    ]\n\n    PostIP_Old = [\n        '◆ From:'\n    ]\n\n    Edit = [\n        '※ 編輯'\n    ]\n\n    PostURL = [\n        '※ 文章網址'\n    ]\n\n    Vote_Type1 = [\n        '◆ 投票名稱',\n        '◆ 投票中止於',\n        '◆ 票選題目描述'\n    ]\n\n    Vote_Type2 = [\n        '投票名稱',\n        '◆ 預知投票紀事',\n    ]\n\n    AnyKey = '任意鍵'\n\n    InTalk = [\n        '【聊天說話】',\n        '線上使用者列表',\n        '查詢網友',\n        '顯示上幾次熱訊'\n    ]\n\n    InUserList = [\n        '休閒聊天',\n        '聊天/寫信',\n        '說明',\n    ]\n\n    InMailBox = [\n        '【郵件選單】',\n        '[~]資源回收筒',\n        '鴻雁往返'\n    ]\n\n    InMailBoxWithCursor = [\n        '【郵件選單】',\n        '[~]資源回收筒',\n    ]\n    InMailBoxWithCursorLen = len(InMailBoxWithCursor)\n\n    InMailMenu = [\n        '【電子郵件】',\n        '我的信箱',\n        '把所有私人資料打包回去',\n        '寄信給帳號站長',\n    ]\n\n    PostNoContent = [\n        '◆ 此文章無內容',\n        AnyKey\n    ]\n\n    InBoardList = [\n        '【看板列表】',\n        '選擇看板',\n        '只列最愛',\n        '已讀/未讀'\n    ]\n\n    use_too_many_resources = [\n        '程式耗用過多計算資源'\n    ]\n\n    Animation = [\n        '★ 這份文件是可播放的文字動畫，要開始播放嗎？'\n    ]\n\n    CursorToGoodbye = MainMenu.copy()\n\n    content_start = '─── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ──'\n    content_end_list = [\n        '--\\n※ 發信站: 批踢踢實業坊',\n        '--\\n※ 發信站: 批踢踢兔(ptt2.cc)',\n        '--\\n※ 發信站: 新批踢踢(ptt2.twbbs.org.tw)'\n    ]\n\n\ndef show(config, screen_queue, function_name=None):\n    if config.log_level != log.DEBUG:\n        return\n\n    if isinstance(screen_queue, list):\n        for Screen in screen_queue:\n            print('-' * 50)\n            try:\n                print(\n                    Screen.encode(\n                        sys.stdin.encoding, \"replace\").decode(\n                        sys.stdin.encoding))\n            except Exception:\n                print(Screen.encode('utf-8', \"replace\").decode('utf-8'))\n    else:\n        print('-' * 50)\n        try:\n            print(screen_queue.encode(\n                sys.stdin.encoding, \"replace\").decode(\n                sys.stdin.encoding))\n        except Exception:\n            print(screen_queue.encode('utf-8', \"replace\").decode('utf-8'))\n\n        print('len:' + str(len(screen_queue)))\n    if function_name is not None:\n        print('錯誤在 ' + function_name + ' 函式發生')\n    print('-' * 50)\n\n\nxy_pattern_h = re.compile('^=ESC=\\[[\\d]+;[\\d]+H')\nxy_pattern_s = re.compile('^=ESC=\\[[\\d]+;[\\d]+s')\n\n\nclass VT100Parser:\n    def _h(self):\n        self._cursor_x = 0\n        self._cursor_y = 0\n\n    def _2j(self):\n        self.screen = [''] * 24\n        self.screen_length = dict()\n\n    def _move(self, x, y):\n        self._cursor_x = x\n        self._cursor_y = y\n\n    def _newline(self):\n        self._cursor_x = 0\n        self._cursor_y += 1\n\n    def _k(self):\n        if self._cursor_x == 0:\n            # nothing happen but cause error\n            return\n        self.screen[self._cursor_y] = self.screen[self._cursor_y][:self._cursor_x]\n\n    def __init__(self, bytes_data, encoding):\n        # self._data = data\n        # https://www.csie.ntu.edu.tw/~r88009/Java/html/Network/vt100.htm\n\n        self._cursor_x = 0\n        self._cursor_y = 0\n        self.screen = [''] * 24\n        self.screen_length = dict()\n\n        data = bytes_data.decode(encoding, errors='replace')\n\n        # remove color\n        data = re.sub('\\x1B\\[[\\d+;]*m', '', data)\n        data = re.sub(r'[\\x1B]', '=ESC=', data)\n        data = re.sub(r'[\\r]', '', data)\n        while ' \\x08' in data:\n            data = re.sub(r' \\x08', '', data)\n\n        # print('---' * 8)\n        # print(encoding)\n        # print(bytes_data)\n        # print(data)\n        # print('---' * 8)\n\n        if '=ESC=[2J' in data:\n            data = data[data.rfind('=ESC=[2J') + len('=ESC=[2J'):]\n\n        count = 0\n        while data:\n            count += 1\n            while True:\n                if not data.startswith('=ESC='):\n                    break\n                if data.startswith('=ESC=[H'):\n                    data = data[len('=ESC=[H'):]\n                    self._h()\n                    continue\n                elif data.startswith('=ESC=[K'):\n                    data = data[len('=ESC=[K'):]\n                    self._k()\n                    continue\n                elif data.startswith('=ESC=[s'):\n                    data = data[len('=ESC=[s'):]\n                    continue\n                break\n\n            xy_result = None\n            xy_result_h = xy_pattern_h.search(data)\n            if not xy_result_h:\n                xy_result_s = xy_pattern_s.search(data)\n                if xy_result_s:\n                    xy_result = xy_result_s\n            else:\n                xy_result = xy_result_h\n\n            if xy_result:\n                xy_part = xy_result.group(0)\n\n                new_y = int(xy_part[6:xy_part.find(';')]) - 1\n                new_x = int(xy_part[xy_part.find(';') + 1: -1])\n                # log.py.info('xy', xy_part, new_x, new_y)\n                self._move(new_x, new_y)\n\n                data = data[len(xy_part):]\n\n            else:\n                if data.startswith('\\n'):\n                    data = data[1:]\n                    self._newline()\n                    continue\n\n                # print(f'-{data[:1]}-{len(data[:1].encode(\"big5-uao\", \"replace\"))}')\n\n                if self._cursor_y not in self.screen_length:\n                    self.screen_length[self._cursor_y] = len(self.screen[self._cursor_y].encode(encoding, 'replace'))\n\n                current_line_length = self.screen_length[self._cursor_y]\n                replace_mode = False\n                if current_line_length < self._cursor_x:\n                    append_space = ' ' * (self._cursor_x - current_line_length)\n                    self.screen[self._cursor_y] += append_space\n                elif current_line_length > self._cursor_x:\n                    replace_mode = True\n\n                next_newline = data.find('\\n')\n                next_newline = 1920 if next_newline < 0 else next_newline\n\n                next_esc = data.find('=ESC=')\n                next_esc = 1920 if next_esc < 0 else next_esc\n                if next_esc == 0:\n                    break\n\n                current_index = min(next_newline, next_esc)\n\n                current_data = data[:current_index]\n                current_data_length = len(current_data.encode(encoding, 'replace'))\n                # print('=', current_data, '=', current_data_length)\n                if replace_mode:\n                    current_line = self.screen[self._cursor_y][:self._cursor_x]\n                    current_line += current_data\n                    current_line += self.screen[self._cursor_y][self._cursor_x + len(current_data):]\n\n                    self.screen[self._cursor_y] = current_line\n                else:\n                    self.screen[self._cursor_y] += current_data\n                    self._cursor_x += current_data_length\n                    self.screen_length[self._cursor_y] = self._cursor_x\n\n                data = data[current_index:]\n\n                # print('\\n'.join(self.screen))\n        # print('\\n'.join(self._screen))\n        # print('=' * 20)\n        # print(data)\n\n        # print('Spend', count, 'cycle')\n        self.screen = '\\n'.join(self.screen)\n\n\nif __name__ == '__main__':\n    # post list\n    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\"\n\n    # main menu\n    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'\n\n    # query post\n    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'\n\n    # test post\n    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'\n\n    p = VT100Parser(screen, 'utf-8')\n    print(p.screen)\n"
  },
  {
    "path": "PyPtt/service.py",
    "content": "import threading\nimport time\nimport uuid\nfrom typing import Optional\n\nfrom . import PTT\nfrom . import check_value\nfrom . import log\n\n\nclass Service:\n\n    def __init__(self, pyptt_init_config: Optional[dict] = None):\n\n        \"\"\"\n\n        這是一個可以在多執行緒中使用的 PyPtt API 服務。\n\n        | 請注意：這僅僅只是 Thread Safe 的實作，對效能並不會有實質上的幫助。\n        | 如果你需要更好的效能，請在每一個線程都使用一個 PyPtt.API 本身。\n\n        Args:\n            pyptt_init_config (dict): PyPtt 初始化設定，請參考 :ref:`初始化設定 <api-init>`。\n\n        Returns:\n            None\n\n        範例::\n\n            from PyPtt import Service\n\n            def api_test(thread_id, service):\n\n                result = service.call('get_time')\n                print(f'thread id {thread_id}', 'get_time', result)\n\n                result = service.call('get_aid_from_url', {'url': 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html'})\n                print(f'thread id {thread_id}', 'get_aid_from_url', result)\n\n                result = service.call('get_newest_index', {'index_type': PyPtt.NewIndex.BOARD, 'board': 'Python'})\n                print(f'thread id {thread_id}', 'get_newest_index', result)\n\n            if __name__ == '__main__':\n                pyptt_init_config = {\n                    # 'language': PyPtt.Language.ENGLISH,\n                }\n\n                service = Service(pyptt_init_config)\n\n                try:\n                    service.call('login', {'ptt_id': 'YOUR_PTT_ID', 'ptt_pw': 'YOUR_PTT_PW'})\n\n                    pool = []\n                    for i in range(10):\n                        t = threading.Thread(target=api_test, args=(i, service))\n                        t.start()\n                        pool.append(t)\n\n                    for t in pool:\n                        t.join()\n\n                    service.call('logout')\n                finally:\n                    service.close()\n        \"\"\"\n        if pyptt_init_config is None:\n            pyptt_init_config = {}\n\n        log_level = pyptt_init_config.get('log_level', log.INFO)\n        self.logger = log.init(log_level, 'service')\n\n        self.logger.info('init')\n\n        self._api = None\n        self._api_init_config = pyptt_init_config\n\n        self._call_queue = []\n        self._call_result = {}\n\n        self._id_pool = set()\n        self._id_pool_lock = threading.Lock()\n\n        self._close = False\n\n        self._thread = threading.Thread(target=self._run, daemon=True)\n        self._thread.start()\n\n        while self._api is None:\n            time.sleep(0.01)\n\n    def _run(self):\n\n        if self._api is not None:\n            self._api.logout()\n            self._api = None\n\n        self._api = PTT.API(**self._api_init_config)\n\n        self.logger.info('start')\n\n        while not self._close:\n            if len(self._call_queue) == 0:\n                time.sleep(0.05)\n                continue\n\n            call = self._call_queue.pop(0)\n\n            func = getattr(self._api, call['api'])\n\n            api_result = None\n            api_exception = None\n            try:\n                api_result = func(**call['args'])\n            except Exception as e:\n                api_exception = e\n\n            self._call_result[call['id']] = {\n                'result': api_result,\n                'exception': api_exception\n            }\n\n    def _get_call_id(self):\n        while True:\n            call_id = uuid.uuid4().hex\n\n            with self._id_pool_lock:\n                if call_id not in self._id_pool:\n                    self._id_pool.add(call_id)\n                    return call_id\n\n    def call(self, api: str, args: Optional[dict] = None):\n\n        if args is None:\n            args = {}\n\n        check_value.check_type(api, str, 'api')\n        check_value.check_type(args, dict, 'args')\n\n        if api not in dir(self._api):\n            raise ValueError(f'api {api} not found')\n\n        call = {\n            'api': api,\n            'id': self._get_call_id(),\n            'args': args\n        }\n        self._call_queue.append(call)\n\n        while call['id'] not in self._call_result:\n            time.sleep(0.01)\n\n        call_result = self._call_result[call['id']]\n        del self._call_result[call['id']]\n\n        with self._id_pool_lock:\n            self._id_pool.remove(call['id'])\n\n        if call_result['exception'] is not None:\n            raise call_result['exception']\n\n        return call_result['result']\n\n    def close(self):\n        self.logger.info('close')\n        self._close = True\n        self._thread.join()\n\n        self.logger.info('done')\n"
  },
  {
    "path": "README.md",
    "content": "![](https://raw.githubusercontent.com/PttCodingMan/PyPtt/master/logo/facebook_cover_photo_2.png)\n# PyPtt\n[![Package Version](https://img.shields.io/pypi/v/PyPtt.svg)](https://pypi.python.org/pypi/PyPtt)\n![PyPI - Downloads](https://img.shields.io/pypi/dm/PyPtt)\n[![test](https://github.com/PyPtt/PyPtt/actions/workflows/test.yml/badge.svg)](https://github.com/PyPtt/PyPtt/actions/workflows/test.yml)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/PyPtt)\n[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)\n[![chatroom icon](https://patrolavia.github.io/telegram-badge/chat.png)](https://t.me/PyPtt)\n[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](http://paypal.me/CodingMan)\n\n#### PyPtt (PTT Library) 是一套 Pure Python PTT API 是目前支援最完整的 PTT API。具備大部分常用功能，無論推文、發文、取得文章、取得信件、寄信、發 P 幣、丟水球，你都可以在這裡找到完整的使用範例\n#### 使用帳號登入，支援使用登入之後才可以使用的功能，例如：推文、發文、寄信、發 P 幣等等\n#### 本專案意旨在提供 PTT 自動化機器人函式庫，無意違反任何 PTT 站方規範。如有牴觸，請馬上告知。\n####\n#### Pypi: https://pypi.org/project/PyPtt/\n<img src=\"https://raw.githubusercontent.com/PyPtt/PyPtt/master/docs/_static/login_1.0.gif\" width=\"560\">\n\n## 安裝\n```bash\npip install PyPtt\n```\n\n## 回報問題\n#### 請參考 [常見問題](https://pyptt.cc/faq.html) 章節\n\n## 加入 PyPtt 社群\n#### 你可以在 Telegram 上找到 PyPtt 社群 [![chatroom icon](https://patrolavia.github.io/telegram-badge/chat.png)](https://t.me/PyPtt)\n\n## 贊助\n#### 如果這個專案對你有幫助，贊助我一杯咖啡吧!!\n####\n#### [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](http://paypal.me/CodingMan)\n\n## 贊助清單\n\n#### leftc\n"
  },
  {
    "path": "docs/CNAME",
    "content": "pyptt.cc"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/api/bucket.rst",
    "content": "bucket\n==========\n\n.. _api-bucket:\n\n.. automodule:: PyPtt.API\n   :members: bucket\n   :noindex:"
  },
  {
    "path": "docs/api/change_pw.rst",
    "content": "change_pw\n==============\n\n.. automodule:: PyPtt.API\n   :members: change_pw\n   :noindex:"
  },
  {
    "path": "docs/api/comment.rst",
    "content": "comment\n==========\n\n.. _api-comment:\n\n.. automodule:: PyPtt.API\n   :members: comment\n   :noindex:"
  },
  {
    "path": "docs/api/del_mail.rst",
    "content": "del_mail\n============\n\n.. _api-del-mail:\n\n.. automodule:: PyPtt.API\n   :members: del_mail\n   :noindex:"
  },
  {
    "path": "docs/api/del_post.rst",
    "content": "del_post\n==============\n\n.. automodule:: PyPtt.API\n   :members: del_post\n   :noindex:"
  },
  {
    "path": "docs/api/get_aid_from_url.rst",
    "content": "get_aid_from_url\n=====================\n\n.. automodule:: PyPtt.API\n   :members: get_aid_from_url\n   :noindex:"
  },
  {
    "path": "docs/api/get_all_boards.rst",
    "content": "get_all_boards\n=================\n\n.. automodule:: PyPtt.API\n   :members: get_all_boards\n   :noindex:"
  },
  {
    "path": "docs/api/get_board_info.rst",
    "content": "get_board_info\n=================\n\n.. _api-get-board-info:\n\n.. automodule:: PyPtt.API\n   :members: get_board_info\n   :noindex:"
  },
  {
    "path": "docs/api/get_bottom_post_list.rst",
    "content": "get_bottom_post_list\n======================\n\n.. automodule:: PyPtt.API\n   :members: get_bottom_post_list\n   :noindex:"
  },
  {
    "path": "docs/api/get_favourite_boards.rst",
    "content": "get_favourite_boards\n==========================\n\n.. _api-get-favourite-boards:\n\n.. automodule:: PyPtt.API\n   :members: get_favourite_boards\n   :noindex:"
  },
  {
    "path": "docs/api/get_mail.rst",
    "content": "get_mail\n============\n\n.. _api-get-mail:\n\n.. automodule:: PyPtt.API\n   :members: get_mail\n   :noindex:"
  },
  {
    "path": "docs/api/get_newest_index.rst",
    "content": "get_newest_index\n=================\n\n.. _api-get-newest-index:\n\n.. automodule:: PyPtt.API\n   :members: get_newest_index\n   :noindex:"
  },
  {
    "path": "docs/api/get_post.rst",
    "content": "get_post\n==========\n\n.. _api-get-post:\n\n.. automodule:: PyPtt.API\n   :members: get_post\n   :noindex:"
  },
  {
    "path": "docs/api/get_time.rst",
    "content": "get_time\n==========\n\n.. _api-get-time:\n\n.. automodule:: PyPtt.API\n   :members: get_time\n   :noindex:"
  },
  {
    "path": "docs/api/get_user.rst",
    "content": "get_user\n==========\n\n.. _api-get-user:\n\n.. automodule:: PyPtt.API\n   :members: get_user\n   :noindex:"
  },
  {
    "path": "docs/api/give_money.rst",
    "content": "give_money\n=============\n\n.. _api-give-money:\n\n.. automodule:: PyPtt.API\n   :members: give_money\n   :noindex:"
  },
  {
    "path": "docs/api/index.rst",
    "content": "APIs\n=============\n| 這是 PyPtt 的 API 文件。\n| 我們在這裡介紹 PyPtt 目前所有支援 PTT, PTT2 的功能。\n\n基本功能\n----------------\n.. toctree::\n\n   init\n   login_logout\n\n文章相關\n----------------\n.. toctree::\n\n   get_post\n   get_newest_index\n   post\n   reply_post\n   del_post\n   comment\n\n信箱相關\n----------------\n.. toctree::\n\n   mail\n   get_mail\n   del_mail\n\n使用者相關\n----------------\n.. toctree::\n\n   give_money\n   get_user\n   search_user\n   change_pw\n\n取得 PTT 資訊\n-------------------\n.. toctree::\n\n   get_time\n   get_all_boards\n   get_favourite_boards\n   get_board_info\n   get_aid_from_url\n   get_bottom_post_list\n\n版主相關\n----------------\n.. toctree::\n\n   set_board_title\n   mark_post\n   bucket\n"
  },
  {
    "path": "docs/api/init.rst",
    "content": "init\n=======\n\n.. _api-init:\n\n.. automodule:: PyPtt.API\n   :members: __init__"
  },
  {
    "path": "docs/api/login_logout.rst",
    "content": "login, logout\n================\n\n.. _api-login-logout:\n\n.. automodule:: PyPtt.API\n   :members: login, logout\n   :noindex:"
  },
  {
    "path": "docs/api/mail.rst",
    "content": "mail\n=============\n\n.. _api-mail:\n\n.. automodule:: PyPtt.API\n   :members: mail\n   :noindex:"
  },
  {
    "path": "docs/api/mark_post.rst",
    "content": "mark_post\n===============\n\n.. _api-mark-post:\n\n.. automodule:: PyPtt.API\n   :members: mark_post\n   :noindex:"
  },
  {
    "path": "docs/api/post.rst",
    "content": "post\n==========\n\n.. _api-post:\n\n.. automodule:: PyPtt.API\n   :members: post\n   :noindex:"
  },
  {
    "path": "docs/api/reply_post.rst",
    "content": "reply_post\n==========\n\n.. _api-reply-post:\n\n.. automodule:: PyPtt.API\n   :members: reply_post\n   :noindex:"
  },
  {
    "path": "docs/api/search_user.rst",
    "content": "search_user\n================\n\n.. _api-search-user:\n\n.. automodule:: PyPtt.API\n   :members: search_user\n   :noindex:"
  },
  {
    "path": "docs/api/set_board_title.rst",
    "content": "set_board_title\n=====================\n\n.. _api-set-board-title:\n\n.. automodule:: PyPtt.API\n   :members: set_board_title\n   :noindex:"
  },
  {
    "path": "docs/changelog.rst",
    "content": "更新日誌\n====================\n| 這裡寫著 PyPtt 的故事。\n|\n| 2022.12.20 PyPtt 1.0.3，logger 改採用以 logging_ 為基底。\n\n.. _logging: https://docs.python.org/3/howto/logging.html\n\n| 2022.12.19 發佈 :doc:`Docker Image <docker>`。\n\n| 2022.12.08 PyPtt 1.0.1, 1.0.2，修正一些小錯誤\n\n| 2022.12.08 PyPtt 1.0.0 正式發布。\n\n| 2021.12.08 PyPtt 新增 :doc:`service` 功能。\n\n| 2022.12.01 開發 頁面改名為 Roadmap。\n\n| 2022.09.19 更換主題為 furo_。\n\n.. _furo: https://sphinx-themes.org/sample-sites/furo/\n\n| 2022.09.14 太棒了！我們終於有更新日誌了。"
  },
  {
    "path": "docs/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Project information -----------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information\n\nimport os\nimport sys\nfrom datetime import datetime\nsys.path.insert(0, os.path.abspath('../'))\n\nimport PyPtt\n\nproject = 'PyPtt'\ncopyright = f'2017 ~ {datetime.now().year}, CodingMan'\nauthor = 'CodingMan'\n\nversion = PyPtt.__version__\nrelease = PyPtt.__version__\n\nhtml_title = f'PyPtt.cc'\n\n# -- General configuration ---------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration\n\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.napoleon',\n    'sphinx_copybutton',\n    'sphinx.ext.autosectionlabel',\n    'sphinx_sitemap',\n]\nautosectionlabel_prefix_document = True\n\nhtml_baseurl = 'https://pyptt.cc/'\nsitemap_url_scheme = \"{link}\"\n\ntemplates_path = ['_templates']\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\nlanguage = 'zh_TW'\n\nhtml_extra_path = ['CNAME', 'robots.txt']\n\n# -- Options for HTML output -------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output\n\nhtml_theme = 'furo'\nhtml_static_path = ['_static']\nhtml_favicon = \"https://raw.githubusercontent.com/PyPtt/PyPtt/master/logo/facebook_profile_image.png\"\n"
  },
  {
    "path": "docs/dev.rst",
    "content": "Development\n================\n如果你想參與開發，請參考以下須知：\n\n開發環境\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n我們建議您使用 virtualenv 來建立獨立的 Python 環境，以避免相依性問題。\n\n.. code-block:: bash\n\n    virtualenv venv\n    source venv/bin/activate\n\n安裝相依套件\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n你可以使用以下指令來安裝相依套件：\n\n.. code-block:: bash\n\n    pip install -r requirements.txt\n\n如果你想更改文件，請安裝開發相依套件：\n\n.. code-block:: bash\n\n    pip install -r docs/requirements.txt\n\n產生文件網頁\n\n.. code-block:: bash\n\n    bash make_doc.sh\n\n你可以在 docs/_build/html/index.html 中查看文件網頁。\n\n執行測試\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n你可以使用以下指令來執行測試：\n\n.. code-block:: python\n\n    python3 tests/*.py\n\n如果有遺漏的測試，請不吝發起 Pull Request。\n\n撰寫文件\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n| 如果你的變更涉及文件，請記得更新文件。\n| 我們使用 Sphinx 來撰寫文件，你可以在 docs/ 中找到文件的原始碼。\n\n建立你的 Pull Request\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n如果你想要貢獻程式碼，請參考以下步驟：\n\n1. Fork 這個專案。\n2. 建立你的特性分支 (`git checkout -b feat/my-new-feature`)。\n3. Commit 你的變更 (`git commit -am 'feat: add some feature`)。\n    commit msg 格式，請參考 `Conventional Commits`_。\n4. Push 到你的分支 (`git push origin feat/my-new-feature`)。\n5. 建立一個新的 Pull Request。\n\n請注意，我們會優先處理符合 `Conventional Commits`_ 的 Pull Request。\n\n.. _Conventional Commits: https://www.conventionalcommits.org/en/v1.0.0/\n"
  },
  {
    "path": "docs/docker.rst",
    "content": "Docker Image\n=================\n\n.. image:: https://img.shields.io/docker/v/codingman000/pyptt/latest\n   :target: https://hub.docker.com/r/codingman000/pyptt\n\n.. image:: https://img.shields.io/docker/pulls/codingman000/pyptt?color=orange\n    :target: https://hub.docker.com/r/codingman000/pyptt\n\n.. image:: https://img.shields.io/docker/image-size/codingman000/pyptt/latest?color=green\n    :target: https://hub.docker.com/r/codingman000/pyptt\n\n.. image:: https://img.shields.io/docker/stars/codingman000/pyptt?color=succes\n    :target: https://hub.docker.com/r/codingman000/pyptt\n\n\n\n| 是的，PyPtt 也支援 Docker Image。\n| 只要一行指令就可以啟動一個 PyPtt 的 Docker Image，並且可以在 Docker Image 中使用 PyPtt。\n|\n| Doc: https://pyptt.cc/docker.html\n| Docker hub: https://hub.docker.com/r/codingman000/pyptt\n| Github: https://github.com/PyPtt/PyPtt_image\n\n安裝\n-----------------\n\n.. code-block:: bash\n\n    docker pull codingman000/pyptt:latest\n\n啟動\n-----------------\n\n.. code-block:: bash\n\n    docker run -d -p 8787:8787 codingman000/pyptt:latest\n\n連線\n-----------------\n\n物件編碼的方法你可以在這裏了解 程式碼_\n\n.. _程式碼: https://github.com/PyPtt/PyPtt_image/blob/main/src/utils.py#L4\n\n.. code-block:: python\n\n    import PyPtt\n    import requests\n\n    from src.utils import object_encode\n    from tests import config\n\n    if __name__ == '__main__':\n        params = {\n            \"api\": \"login\",\n            \"args\": object_encode({\n                'ptt_id': config.PTT_ID,\n                'ptt_pw': config.PTT_PW\n            })\n        }\n        r = requests.get(\"http://localhost:8787/api\", params=params)\n        print(r.json())\n\n        params = {\n            \"api\": \"get_time\",\n        }\n        r = requests.get(\"http://localhost:8787/api\", params=params)\n        print(r.json())\n\n        params = {\n            \"api\": \"get_newest_index\",\n            \"args\": object_encode({\n                'board': 'Gossiping',\n                'index_type': PyPtt.NewIndex.BOARD\n            })\n        }\n        r = requests.get(\"http://localhost:8787/api\", params=params)\n        print(r.json())\n\n        ##############################\n\n        content = \"\"\"此內容由 PyPtt image 執行 PO 文\n\n        測試換行 123\n        測試換行 456\n        測試換行 789\n        \"\"\"\n\n        params = {\n            \"api\": \"post\",\n            \"args\": object_encode({\n                'board': 'Test',\n                'title_index': 1,\n                'title': 'test',\n                'content': content,\n            })\n        }\n        r = requests.get(\"http://localhost:8787/api\", params=params)\n        print(r.json())\n\n        ##############################\n\n        params = {\n            \"api\": \"logout\",\n        }\n        r = requests.get(\"http://localhost:8787/api\", params=params)\n        print(r.json())"
  },
  {
    "path": "docs/examples.rst",
    "content": "使用範例\n=============\n| 這裡記錄了各種實際使用的範例 ☺️\n\n保持登入\n--------\n這裡示範了如何保持登入\n\n.. code-block:: python\n\n    import PyPtt\n\n    def login():\n        max_retry = 5\n\n        ptt_bot = None\n        for retry_time in range(max_retry):\n            try:\n                ptt_bot = PyPtt.API()\n\n                ptt_bot.login('YOUR_ID', 'YOUR_PW',\n                    kick_other_session=False if retry_time == 0 else True)\n                break\n            except PyPtt.exceptions.LoginError:\n                ptt_bot = None\n                print('登入失敗')\n                time.sleep(3)\n            except PyPtt.exceptions.LoginTooOften:\n                ptt_bot = None\n                print('請稍後再試')\n                time.sleep(60)\n            except PyPtt.exceptions.WrongIDorPassword:\n                print('帳號密碼錯誤')\n                raise\n            except Exception as e:\n                print('其他錯誤:', e)\n                break\n\n        return ptt_bot\n\n    if __name__ == '__main__':\n        login()\n\n        last_newest_index = ptt_bot.get_newest_index()\n        time.sleep(60)\n\n        try:\n            while True:\n\n                try:\n                    newest_index = ptt_bot.get_newest_index()\n                except PyPtt.exceptions.ConnectionClosed:\n                    ptt_bot = login()\n                    continue\n                except Exception as e:\n                    print('其他錯誤:', e)\n                    break\n\n                if newest_index == last_newest_index:\n                    continue\n\n                print('有新文章!', newest_index)\n\n                # do something\n\n                time.sleep(5)\n        finally:\n            ptt_bot.logout()\n\n幫你的文章上色\n--------------\n如果在發的時候有上色的需求，可以透過模擬鍵盤輸入的方式達到加上色碼的效果\n\n.. code-block:: python\n\n    import PyPtt\n\n    content = [\n        PTT.command.Ctrl_C + PTT.command.Left + '5' + PTT.command.Right + '這是閃爍字' + PTT.command.Ctrl_C,\n        PTT.command.Ctrl_C + PTT.command.Left + '31' + PTT.command.Right + '前景紅色' + PTT.command.Ctrl_C,\n        PTT.command.Ctrl_C + PTT.command.Left + '44' + PTT.command.Right + '背景藍色' + PTT.command.Ctrl_C,\n    ]\n    content = '\\n'.join(content)\n\n    ptt_bot = PyPtt.API()\n    try:\n        # .. login ..\n        ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content=content, sign_file=0)\n    finally:\n        ptt_bot.logout()\n\n.. image:: _static/color_demo.png\n\n.. _check_post_status:\n\n如何判斷文章資料是否可以使用\n------------------------------\n當 :doc:`api/get_post` 回傳文章資料回來時，這時需要一些判斷來決定是否要使用這些資料\n\n.. code-block:: python\n\n    import PyPtt\n\n    ptt_bot = PyPtt.API()\n    try:\n        # .. login ..\n        post_info = ptt_bot.get_post('Python', index=1)\n\n        print(post_info)\n\n        if post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.EXISTS:\n            print('文章存在！')\n        elif post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.DELETED_BY_AUTHOR:\n            print('文章被作者刪除')\n            sys.exit()\n        elif post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.DELETED_BY_MODERATOR:\n            print('文章被版主刪除')\n            sys.exit()\n\n        if not post_info[PyPtt.PostField.pass_format_check]:\n            print('未通過格式檢查')\n            sys.exit()\n\n        print('文章資料可以使用')\n    finally:\n        ptt_bot.logout()\n"
  },
  {
    "path": "docs/exceptions.rst",
    "content": "例外\n========\n| 這裡介紹 PyPtt 的例外。\n| 可以用 try...except... 來處理。\n\n| 例外的種類\n\n.. py:exception:: PyPtt.exceptions.RequireLogin\n    :module: PyPtt\n\n    需要登入。\n\n.. py:exception:: PyPtt.exceptions.NoPermission\n    :module: PyPtt\n\n    沒有權限。\n\n.. py:exception:: PyPtt.exceptions.LoginError\n    :module: PyPtt\n\n    登入失敗。\n\n.. py:exception:: PyPtt.exceptions.NoFastComment\n    :module: PyPtt\n\n    無法快速推文。\n\n.. py:exception:: PyPtt.exceptions.NoSuchUser\n    :module: PyPtt\n\n    查無此使用者。\n\n.. py:exception:: PyPtt.exceptions.NoSuchMail\n    :module: PyPtt\n\n    查無此信件。\n\n.. py:exception:: PyPtt.exceptions.NoMoney\n    :module: PyPtt\n\n    餘額不足。\n\n.. py:exception:: PyPtt.exceptions.NoSuchBoard\n    :module: PyPtt\n\n    查無此看板。\n\n.. py:exception:: PyPtt.exceptions.ConnectionClosed\n    :module: PyPtt\n\n    連線已關閉。\n\n.. py:exception:: PyPtt.exceptions.UnregisteredUser\n    :module: PyPtt\n\n    未註冊使用者。\n\n.. py:exception:: PyPtt.exceptions.MultiThreadOperated\n    :module: PyPtt\n\n    同時使用多個 thread 呼叫 PyPtt 。\n\n.. py:exception:: PyPtt.exceptions.WrongIDorPassword\n    :module: PyPtt\n\n    帳號或密碼錯誤。\n\n.. py:exception:: PyPtt.exceptions.WrongPassword\n    :module: PyPtt\n\n    密碼錯誤。\n\n.. py:exception:: PyPtt.exceptions.LoginTooOften\n    :module: PyPtt\n\n    登入太頻繁。\n\n.. py:exception:: PyPtt.exceptions.UseTooManyResources\n    :module: PyPtt\n\n    使用過多資源。\n\n.. py:exception:: PyPtt.exceptions.HostNotSupport\n    :module: PyPtt\n\n    主機不支援。詳見 :ref:`host`。\n\n.. py:exception:: PyPtt.exceptions.CantComment\n    :module: PyPtt\n\n    禁止推文。\n\n.. py:exception:: PyPtt.exceptions.CantResponse\n    :module: PyPtt\n\n    已結案並標記, 不得回應。\n\n.. py:exception:: PyPtt.exceptions.NeedModeratorPermission\n    :module: PyPtt\n\n    需要版主權限。\n\n.. py:exception:: PyPtt.exceptions.ConnectError\n    :module: PyPtt\n\n    連線失敗。\n\n.. py:exception:: PyPtt.exceptions.NoSuchPost\n    :module: PyPtt\n\n    文章不存在。\n\n.. py:exception:: PyPtt.exceptions.CanNotUseSearchPostCode\n    :module: PyPtt\n\n    無法使用搜尋文章代碼。\n\n.. py:exception:: PyPtt.exceptions.UserHasPreviouslyBeenBanned\n    :module: PyPtt\n\n    `水桶`_ 使用者，但已經被 `水桶`_。\n\n.. py:exception:: PyPtt.exceptions.MailboxFull\n    :module: PyPtt\n\n    信箱已滿。\n\n.. py:exception:: PyPtt.exceptions.NoSearchResult\n    :module: PyPtt\n\n    搜尋結果為空。\n\n.. py:exception:: PyPtt.exceptions.OnlySecureConnection\n    :module: PyPtt\n\n    只能使用安全連線。\n\n.. py:exception:: PyPtt.exceptions.SetContactMailFirst\n    :module: PyPtt\n\n    請先設定聯絡信箱。\n\n.. py:exception:: PyPtt.exceptions.ResetYourContactEmail\n    :module: PyPtt\n\n    請重新設定聯絡信箱。\n\n.. _水桶: https://pttpedia.fandom.com/zh/wiki/%E6%B0%B4%E6%A1%B6"
  },
  {
    "path": "docs/faq.rst",
    "content": "FAQ\n==========\n這裡搜集了一些常見問題的解答，如果你有任何問題，請先看看這裡。\n\nQ: 我該如何使用 PyPtt？\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n| A: 可以先參考 :doc:`install`、:doc:`api/index` 與 :doc:`examples`。\n\nQ: 使用 PyPtt 時，遇到問題該如何解決？\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n1. 自己修正並提交 PR，如果沒辦法請參考步驟 2。\n    * 如果你是程式設計師，可以參考 :doc:`參與開發 <dev>` 一起幫忙修正問題。\n2. 到 GitHub 提出 `issue`_ 或者到 `PyPtt Telegram 社群`_ 討論。\n    * 請先確認你使用的版本是否為 |version_pic|，如果不是，請更新到最新版本。\n    * 如果你使用的是最新版本，請確認你的問題是否已經在這裡被回答過了。\n    * 如果你的問題還沒有被回答過，請依照以下程式碼將 LogLevel_ 設定為 `DEBUG`，並附上 **可以重現問題的程式碼**。\n    * 到 GitHub 提出 `issue`_ 或者到 `PyPtt Telegram 社群`_ 討論。\n\n.. code-block:: python\n\n    import PyPtt\n\n    ptt_bot = PyPtt.API(log_level=PyPtt.LogLevel.DEBUG)\n\n    # 你的程式碼\n\n.. |version_pic| image:: https://img.shields.io/pypi/v/PyPtt.svg\n    :target: https://pypi.org/project/PyPtt/\n\n.. _`PyPtt Telegram 社群`: https://t.me/PyPtt\n\n.. _LogLevel: https://github.com/PttCodingMan/SingleLog/blob/d7c19a1b848dfb1c9df8201f13def9a31afd035c/SingleLog/SingleLog.py#L22\n\n.. _`issue`: https://github.com/PyPtt/PyPtt/issues/new\n\nQ: 在 jupyter 遭遇 `the event loop is already running` 錯誤\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n| A: 因為 jupyter 內部也使用了 asyncio 作為協程管理工具，會跟 PyPtt 內部的 asyncio 衝突，所以如果想要在 jypyter 內使用，請在你的程式碼中加入以下程式碼\n\n.. code-block:: bash\n    :caption: 安裝 nest_asyncio\n\n    ! pip install nest_asyncio\n\n\n\n.. code-block:: python\n    :caption: 在程式碼中引用 nest_asyncio\n\n    import nest_asyncio\n    nest_asyncio.apply()\n\nQ: 在 Mac 無法使用 WebSocket 連線，遭遇 SSL 相關錯誤\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n| A: 請參考以下指令，安裝 Python 的 SSL 憑證\n\n.. code-block:: bash\n    :caption: 以 Python 3.10 為例\n\n    sh /Applications/Python\\ 3.10/Install\\ Certificates.command\n\nQ: 為什麼我沒辦法在雲端環境上使用 PyPtt？\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n| A: 如果你是使用雲端 (Colab、GCP、Azure、AWS...etc) 無法連線 PTT 是正常的。\n| 因為 PTT 有防止機器人登入的機制，所以在雲端環境上無法使用 PyPtt。"
  },
  {
    "path": "docs/index.rst",
    "content": "PyPtt\n====================\n\n.. image:: _static/logo_cover.png\n    :alt: PyPtt: PTT bot library for Python\n    :align: center\n\n.. image:: https://img.shields.io/pypi/v/PyPtt.svg\n    :target: https://pypi.org/project/PyPtt/\n\n.. image:: https://img.shields.io/github/last-commit/pyptt/pyptt.svg?color=green\n    :target: https://github.com/PyPtt/PyPtt/commits/\n\n.. image:: https://img.shields.io/pypi/dm/PyPtt?color=ocean\n    :target: https://pypi.org/project/PyPtt/\n\n.. image:: https://github.com/PyPtt/PyPtt/actions/workflows/test.yml/badge.svg?branch=master&color=yellogreen\n    :target: https://github.com/PyPtt/PyPtt/actions/workflows/test.yml\n\n.. image:: https://img.shields.io/pypi/pyversions/PyPtt\n    :target: https://pypi.org/project/PyPtt/\n\n.. image:: https://img.shields.io/badge/License-LGPL%20v3-blue.svg\n    :target: https://www.gnu.org/licenses/lgpl-3.0\n\n.. image:: https://img.shields.io/github/stars/pyptt/pyptt?style=social\n    :target: https://github.com/PyPtt/PyPtt/stargazers\n\n\n| PyPtt_ 是時下最流行的 PTT library，你可以在 Python 程式碼裡面使用 PTT 常見的操作，例如：:doc:`推文 <api/comment>`、:doc:`發文 <api/post>`、:doc:`寄信 <api/mail>`、:doc:`讀取信件 <api/get_mail>`、:doc:`讀取文章 <api/get_post>` 等等操作。\n|\n| 本文件的內容會隨著 PyPtt_ 的更新而更新，如果你發現任何錯誤，歡迎到 PyPtt_ 發 issue 或者加入 `PyPtt Telegram 社群`_ 一起討論。\n|\n| PyPtt 由 CodingMan_ 與其他許多的 `貢獻者`_ 共同維護。\n\n.. _PyPtt: https://github.com/PyPtt/PyPtt\n.. _`PyPtt Telegram 社群`: https://t.me/PyPtt\n.. _CodingMan: https://github.com/PttCodingMan\n.. _`貢獻者`: https://github.com/PyPtt/PyPtt/graphs/contributors\n\n重要消息\n--------------------\n| 2022.12.19 發佈 :doc:`Docker Image <docker>`。\n| 2022.12.08 PyPtt 1.0.0 正式發布\n| 2021.12.08 PyPtt 新增 :doc:`service` 功能\n\n\n文件\n----------------\n:doc:`安裝 PyPtt <install>`\n    如何把 PyPtt 安裝到你的環境中。\n\n:doc:`APIs <api/index>`\n    PyPtt 的所有 API 說明。\n\n:doc:`Service <service>`\n    如何在多線程的情況，安全地使用 PyPtt。\n\n:doc:`參數型態 <type>`\n    PyPtt 的所有參數型態選項。\n\n:doc:`例外 <exceptions>`\n    PyPtt 所有你可能遭遇到的錯誤。\n\n:doc:`使用範例 <examples>`\n    一些使用 PyPtt 的範例。\n\n:doc:`Docker Image <docker>`\n    如何使用 Docker Image 來使用 PyPtt。\n\n:doc:`參與開發 <dev>`\n    如果你想要貢獻 PyPtt，可以看看這裡。\n\n:doc:`常見問題 <faq>`\n    任何常見問題都可以在這找到解答。\n\n:doc:`Roadmap <roadmap>`\n    | 這裡列了我們正在做什麼與打算做什麼。\n    | 如果你想要貢獻 PyPtt，可以看看這裡。\n\n:doc:`ChangeLog <changelog>`\n    | 我們曾經做了什麼。\n\n.. toctree::\n    :maxdepth: 3\n    :caption: 目錄\n    :hidden:\n\n    install\n    api/index\n    service\n    type\n    exceptions\n    examples\n    docker image <docker>\n    參與開發 <dev>\n    常見問題 <faq>\n    Roadmap <roadmap>\n    ChangeLog <changelog>\n\n    Source Code <https://github.com/PyPtt/>\n    PyPI <https://pypi.org/project/PyPtt/>\n"
  },
  {
    "path": "docs/install.rst",
    "content": "安裝 PyPtt\n===================\n\nPython 版本\n--------------\n| 推薦使用 CPython_ 3.8+。\n\n.. _CPython: https://www.python.org/\n\n相依套件\n--------------\nPyPtt 目前相依於以下套件，這些套件都會在安裝的過程中被自動安裝。\n\n* progressbar2_ is a text progress bar library for Python.\n* websockets_ is a library for building WebSocket_ servers and clients in Python with a focus on correctness, simplicity, robustness, and performance.\n* uao_ is a pure Python implementation of the Unicode encoder/decoder.\n* requests_ is a Python HTTP library, released under the Apache License 2.0.\n* AutoStrEnum_ is a Python library that provides an Enum class that automatically converts enum values to and from strings.\n* PyYAML_ is a YAML parser and emitter for Python.\n\n.. _progressbar2: https://progressbar-2.readthedocs.io/en/latest/\n.. _websockets: https://websockets.readthedocs.io/en/stable/\n.. _`WebSocket`: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API\n.. _uao: https://github.com/eight04/pyUAO\n.. _requests: https://requests.readthedocs.io/en/master/\n.. _AutoStrEnum: https://github.com/PttCodingMan/PttCodingMan\n.. _PyYAML: https://pyyaml.org/\n\n使用虛擬環境安裝 (推薦)\n-------------------------\n| 我們推薦各位使用虛擬環境 venv_ 來安裝 PyPtt，因為可以盡可能地避免套件衝突。\n|\n| 你可以從 `Virtual Environments and Packages`_ 中了解，更多關於使用虛擬環境的理由以及如何建立你的虛擬環境。\n\n.. _`Virtual Environments and Packages`: https://docs.python.org/3/tutorial/venv.html#tut-venv\n.. _venv: https://docs.python.org/3/library/venv.html\n\n安裝指令\n----------------\n你可以使用以下指令來安裝 PyPtt。\n\n.. code-block:: bash\n\n    pip install PyPtt\n\n現在 PyPtt 已經成功安裝了，來看看 PyPtt 的 :doc:`API 說明 <api/index>` 或者 :doc:`使用範例 <examples>` 吧！\n\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "PyPtt\nsphinx\nsphinx-copybutton\npygments==2.15.0\nFuro\nsphinx-sitemap"
  },
  {
    "path": "docs/roadmap.rst",
    "content": "開發\n=============\n| 這裡列了一些我們正在開發的功能，如果你有任何建議，歡迎找我們聊聊。\n| 或者你也可以直接在 github 上開 issue，我們會盡快回覆。\n\n| 當然如果你有興趣參與開發，也歡迎你加入我們，我們會盡快回覆你的加入申請。\n\n未來開發計劃\n--------------------\n* WebSocket 支援 Tor or Proxy\n    期待有一天可以透過 Tor 或 Proxy 來連接到 PTT，讓 PyPtt 可以自由地在雲端伺服器運作。\n\n開發中\n--------------------\n* 支援 PTT 官方 APP API\n    你可以在 ptt-app-api_ 分支上找到目前的進度。\n\n.. _ptt-app-api: https://github.com/PyPtt/PyPtt/tree/ptt-app-api\n\n已完成\n--------------------\n* PyPtt Service docker\n    | 期待 PyPtt 在未來可以有 API 形式的服務，讓大家可以透過 API 呼叫來使用 PyPtt。\n    | 這樣其實在某個層面也上可以達到使用 Tor or Proxy 的目的。\n\n    * PyPtt :doc:`service` 2022.12.18 完成\n    * Docker :doc:`docker` 2022.12.19 完成\n* 官方網站的建置 2022.12.18 完成\n     使用 sphinx 來建置官方網站，讓大家可以更方便地了解 PyPtt。\n* 測試案例 2022.12.15 完成\n* 1.0 正式版本重構 2022.11.15 完成"
  },
  {
    "path": "docs/robots.txt",
    "content": "User-agent: *\n\nSitemap: https://pyptt.cc/sitemap.xml"
  },
  {
    "path": "docs/service.rst",
    "content": "Service\n===========\n\n\n.. automodule:: PyPtt.Service\n    :members: __init__\n    :undoc-members:\n"
  },
  {
    "path": "docs/type.rst",
    "content": "參數型態\n===========\n這裡介紹 PyPtt 的參數型態\n\n.. _host:\n\nHOST\n-----------\n* 連線的 PTT 伺服器。\n\n.. py:attribute:: PyPtt.HOST.PTT1\n\n    批踢踢實業坊\n\n.. py:attribute:: PyPtt.HOST.PTT2\n\n    批踢踢兔\n\n.. _language:\n\nLanguage\n-----------\n* 顯示訊息的語言。\n\n.. py:attribute:: PyPtt.Language.MANDARIN\n\n    繁體中文\n\n.. py:attribute:: PyPtt.Language.ENGLISH\n\n    英文\n\n.. _connect-mode:\n\nConnectMode\n-----------\n* 連線的模式。\n\n.. py:attribute:: PyPtt.ConnectMode.WEBSOCKETS\n\n    使用 WEBSOCKETS 連線\n\n.. py:attribute:: PyPtt.ConnectMode.TELNET\n\n    使用 TELNET 連線\n\n.. _new-index:\n\nNewIndex\n-----------\n* 搜尋 Index 的種類。\n\n.. py:attribute:: PyPtt.NewIndex.BOARD\n\n    搜尋看板 Index\n\n.. py:attribute:: PyPtt.NewIndex.MAIL\n\n    搜尋信箱 Index\n\n.. _search-type:\n\nSearchType\n-----------\n* 搜尋看板的方式。\n\n.. py:attribute:: PyPtt.SearchType.KEYWORD\n\n    搜尋關鍵字\n\n.. py:attribute:: PyPtt.SearchType.AUTHOR\n\n    搜尋作者\n\n.. py:attribute:: PyPtt.SearchType.COMMENT\n\n    搜尋推文數\n\n.. py:attribute:: PyPtt.SearchType.MARK\n\n    搜尋標記\n\n.. py:attribute:: PyPtt.SearchType.MONEY\n\n    搜尋稿酬\n\n.. _reply-to:\n\nReplyTo\n-----------\n* 回文的方式。\n\n.. py:attribute:: PyPtt.ReplyTo.BOARD\n\n    回文至看板\n\n.. py:attribute:: PyPtt.ReplyTo.MAIL\n\n    回文至信箱\n\n.. py:attribute:: PyPtt.ReplyTo.BOARD_MAIL\n\n    回文至看板與信箱\n\n.. _comment-type:\n\nCommentType\n-----------\n* 推文方式。\n\n.. py:attribute:: PyPtt.CommentType.PUSH\n\n    推\n\n.. py:attribute:: PyPtt.CommentType.BOO\n\n    噓\n\n.. py:attribute:: PyPtt.CommentType.ARROW\n\n    箭頭\n\n.. _post-status:\n\nPostStatus\n-----------\n* 文章狀態。\n\n.. py:attribute:: PyPtt.PostStatus.EXISTS\n\n    文章存在\n\n.. py:attribute:: PyPtt.PostStatus.DELETED_BY_AUTHOR\n\n    被作者刪除\n\n.. py:attribute:: PyPtt.PostStatus.DELETED_BY_MODERATOR\n\n    被板主刪除\n\n.. py:attribute:: PyPtt.PostStatus.DELETED_BY_UNKNOWN\n\n    無法判斷，被如何刪除\n\n.. _mark-type:\n\nMarkType\n-----------\n* 版主標記文章種類\n\n.. py:attribute:: PyPtt.MarkType.S\n\n    S 文章\n\n.. py:attribute:: PyPtt.MarkType.D\n\n    標記文章\n\n.. py:attribute:: PyPtt.MarkType.DELETE_D\n\n    刪除標記文章\n\n.. py:attribute:: PyPtt.MarkType.M\n\n    M 起來\n\n.. py:attribute:: PyPtt.MarkType.UNCONFIRMED\n\n    待證實文章\n\n\n.. _user-field:\n\nUserField\n-----------\n* 使用者資料欄位。\n\n.. py:attribute:: PyPtt.UserField.ptt_id\n\n    使用者 ID\n\n.. py:attribute:: PyPtt.UserField.money\n\n    經濟狀態\n\n.. py:attribute:: PyPtt.UserField.login_count\n\n    登入次數\n\n.. py:attribute:: PyPtt.UserField.account_verified\n\n    是否通過認證\n\n.. py:attribute:: PyPtt.UserField.legal_post\n\n    文章數量\n\n.. py:attribute:: PyPtt.UserField.illegal_post\n\n    退文數量\n\n.. py:attribute:: PyPtt.UserField.activity\n\n    目前動態\n\n.. py:attribute:: PyPtt.UserField.mail\n\n    信箱狀態\n\n.. py:attribute:: PyPtt.UserField.last_login_date\n\n    最後登入時間\n\n.. py:attribute:: PyPtt.UserField.last_login_ip\n\n    最後登入 IP\n\n.. py:attribute:: PyPtt.UserField.five_chess\n\n    五子棋戰積\n\n.. py:attribute:: PyPtt.UserField.chess\n\n    象棋戰積\n\n.. py:attribute:: PyPtt.UserField.signature_file\n\n    簽名檔\n\n.. _comment-field:\n\nCommentField\n--------------\n* 推文資料欄位。\n\n.. py:attribute:: PyPtt.CommentField.type\n\n    推文類型，推噓箭頭，詳見 :ref:`comment-type`\n\n.. py:attribute:: PyPtt.CommentField.author\n\n    推文作者\n\n.. py:attribute:: PyPtt.CommentField.content\n\n    推文內容\n\n.. py:attribute:: PyPtt.CommentField.ip\n\n    推文 IP (如果存在)\n\n.. py:attribute:: PyPtt.CommentField.time\n\n    推文時間\n\n.. _favorite-board-field:\n\nFavouriteBoardField\n--------------------\n* 我的最愛資料欄位。\n\n.. py:attribute:: PyPtt.FavouriteBoardField.board\n\n    看板名稱\n\n.. py:attribute:: PyPtt.FavouriteBoardField.title\n\n    看板標題\n\n.. py:attribute:: PyPtt.FavouriteBoardField.type\n\n    類別\n\n.. _mail-field:\n\nMailField\n----------\n* 信件資料欄位。\n\n.. py:attribute:: PyPtt.MailField.origin_mail\n\n    原始信件全文\n\n.. py:attribute:: PyPtt.MailField.author\n\n    信件作者\n\n.. py:attribute:: PyPtt.MailField.title\n\n    信件標題\n\n.. py:attribute:: PyPtt.MailField.date\n\n    信件日期\n\n.. py:attribute:: PyPtt.MailField.content\n\n    信件內容\n\n.. py:attribute:: PyPtt.MailField.ip\n\n    信件 IP\n\n.. py:attribute:: PyPtt.MailField.location\n\n    信件位置\n\n.. py:attribute:: PyPtt.MailField.is_red_envelope\n\n    是否為紅包\n\n.. _board-field:\n\nBoardField\n-----------\n* 看板資料欄位。\n\n.. py:attribute:: PyPtt.BoardField.board\n\n    看板名稱\n\n.. py:attribute:: PyPtt.BoardField.online_user\n\n    在線人數\n\n.. py:attribute:: PyPtt.BoardField.chinese_des\n\n    看板中文名稱\n\n.. py:attribute:: PyPtt.BoardField.moderators\n\n    看板板主清單\n\n.. py:attribute:: PyPtt.BoardField.open_status\n\n    看板公開狀態，是否隱板\n\n.. py:attribute:: PyPtt.BoardField.into_top_ten_when_hide\n\n    隱板時是否可以進入十大排行榜\n\n.. py:attribute:: PyPtt.BoardField.can_non_board_members_post\n\n    非看板成員是否可以發文\n\n.. py:attribute:: PyPtt.BoardField.can_reply_post\n\n    是否可以回覆文章\n\n.. py:attribute:: PyPtt.BoardField.self_del_post\n\n    是否可以自刪文章\n\n.. py:attribute:: PyPtt.BoardField.can_comment_post\n\n    是否可以推文\n\n.. py:attribute:: PyPtt.BoardField.can_boo_post\n\n    是否可以噓文\n\n.. py:attribute:: PyPtt.BoardField.can_fast_push\n\n    是否可以快速推文\n\n.. py:attribute:: PyPtt.BoardField.min_interval_between_comments\n\n    推文間隔時間\n\n.. py:attribute:: PyPtt.BoardField.is_comment_record_ip\n\n    是否記錄推文 IP\n\n.. py:attribute:: PyPtt.BoardField.is_comment_aligned\n\n    推文是否對齊\n\n.. py:attribute:: PyPtt.BoardField.can_moderators_del_illegal_content\n\n    板主是否可以刪除違規文字\n\n.. py:attribute:: PyPtt.BoardField.does_tran_post_auto_recorded_and_require_post_permissions\n\n    是否自動記錄轉錄文章並需要發文權限\n\n.. py:attribute:: PyPtt.BoardField.is_cool_mode\n\n    是否為冷板模式\n\n.. py:attribute:: PyPtt.BoardField.is_require18\n\n    是否為 18 禁看板\n\n.. py:attribute:: PyPtt.BoardField.require_login_time\n\n    發文需要登入次數\n\n.. py:attribute:: PyPtt.BoardField.require_illegal_post\n\n    發文需要最低退文數量\n\n.. py:attribute:: PyPtt.BoardField.post_kind_list\n\n    發文類別，例如 [公告] [問卦] 等\n\n.. _post-field:\n\nPostField\n-----------\n* 文章資料欄位。\n\n.. py:attribute:: PyPtt.PostField.board\n\n    文章所在看板\n\n.. py:attribute:: PyPtt.PostField.aid\n\n    文章 ID，例如：`#1Z69g2ts`\n\n.. py:attribute:: PyPtt.PostField.index\n\n    文章編號，例如：906\n\n.. py:attribute:: PyPtt.PostField.author\n\n    文章作者\n\n.. py:attribute:: PyPtt.PostField.date\n\n    文章日期\n\n.. py:attribute:: PyPtt.PostField.title\n\n    文章標題\n\n.. py:attribute:: PyPtt.PostField.content\n\n    文章內容\n\n.. py:attribute:: PyPtt.PostField.money\n\n    文章稿酬，P 幣\n\n.. py:attribute:: PyPtt.PostField.url\n\n    文章網址\n\n.. py:attribute:: PyPtt.PostField.ip\n\n    文章 IP\n\n.. py:attribute:: PyPtt.PostField.comments\n\n    文章推文清單，詳見 :ref:`comment-field`\n\n.. py:attribute:: PyPtt.PostField.post_status\n\n    文章狀態，詳見 :ref:`post-status`\n\n.. py:attribute:: PyPtt.PostField.list_date\n\n    文章列表日期\n\n.. py:attribute:: PyPtt.PostField.has_control_code\n\n    文章是否有控制碼\n\n.. py:attribute:: PyPtt.PostField.pass_format_check\n\n    文章是否通過格式檢查\n\n.. py:attribute:: PyPtt.PostField.location\n\n    文章 IP 位置\n\n.. py:attribute:: PyPtt.PostField.push_number\n\n    文章推文數量\n\n.. py:attribute:: PyPtt.PostField.is_lock\n\n    文章是否鎖定\n\n.. py:attribute:: PyPtt.PostField.full_content\n\n    文章完整內容\n\n.. py:attribute:: PyPtt.PostField.is_unconfirmed\n\n    文章是否為未確認文章"
  },
  {
    "path": "make_doc.sh",
    "content": "make -C docs/ clean\nmake -C docs/ html"
  },
  {
    "path": "requirements.txt",
    "content": "progressbar2\nwebsockets\nuao\nrequests==2.31.0\nAutoStrEnum\nPyYAML"
  },
  {
    "path": "scripts/lang.py",
    "content": "import json\nimport os\nimport sys\nfrom collections import defaultdict\n\nimport yaml\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\n\n\ndef add_lang():\n    new_words = [\n        (PyPtt.Language.MANDARIN, 'give_money', '給 _target0_ _target_ P 幣'),\n        (PyPtt.Language.ENGLISH, 'give_money', 'give _target0_ _target_ P coins'),\n    ]\n\n    for lang, key, value in new_words:\n        PyPtt.i18n.init(lang, cache=True)\n\n        PyPtt.i18n._lang_data[key] = value\n\n        with open(f'PyPtt/lang/{lang}.yaml', 'w', encoding='utf-8') as f:\n            yaml.dump(PyPtt.i18n._lang_data, f, allow_unicode=True, default_flow_style=False)\n\n\ndef check_lang():\n    import re\n\n    # 搜尋 PyPtt 資料夾底下，所有用到 i18n 的字串\n\n    PyPtt.i18n.init(PyPtt.Language.MANDARIN, cache=True)\n\n    # init count dict\n    count_dict = {}\n    for key, value in PyPtt.i18n._lang_data.items():\n        print('->', key, value)\n        count_dict[key] = 0\n\n    # 1. 用 os.walk() 搜尋所有檔案\n    for dirpath, dirnames, filenames in os.walk('./PyPtt'):\n        print(f'================= directory: {dirpath}')\n        for file_name in filenames:\n\n            if not file_name.endswith('.py'):\n                continue\n\n            if file_name == 'i18n.py':\n                continue\n\n            print(file_name)\n\n            with open(f'{dirpath}/{file_name}', 'r', encoding='utf-8') as f:\n                data = f.read()\n\n            for match in re.finditer(r'i18n\\.(\\w+)', data):\n                # print(match.group(0))\n                # print(match.group(1))\n\n                data_key = match.group(1)\n\n                if data_key not in count_dict:\n                    print(f'Unknown key: {data_key}')\n                else:\n                    count_dict[data_key] += 1\n                print('-----------------')\n\n    print(json.dumps(count_dict, indent=4, ensure_ascii=False))\n\n    # collect the keys with 0 count\n    zero_count_keys = [key for key, value in count_dict.items() if value == 0]\n\n    for lang in PyPtt.i18n.locale_pool:\n        PyPtt.i18n.init(lang, cache=True)\n        for key in zero_count_keys:\n            # remove the key from the lang data\n            PyPtt.i18n._lang_data.pop(key, None)\n            print(f'Removed key: {key} from {lang}.yaml')\n\n        with open(f'PyPtt/lang/{lang}.yaml', 'w', encoding='utf-8') as f:\n            yaml.dump(PyPtt.i18n._lang_data, f, allow_unicode=True, default_flow_style=False)\n\nif __name__ == '__main__':\n    add_lang()\n    # check_lang()\n    pass\n\n"
  },
  {
    "path": "scripts/package_script.py",
    "content": "import os\nimport subprocess\nimport time\n\n\ndef get_next_version():\n    is_merged = os.environ.get('GITHUB_EVENT_NAME') == 'pull_request' and os.environ.get(\n        'GITHUB_EVENT_ACTION') == 'closed'\n    print('is_merged:', is_merged)\n\n    # read the main version from __init__.py\n    with open('PyPtt/__init__.py', 'r', encoding='utf-8') as f:\n        data = f.read().strip()\n        main_version = data.split('_main_version = ')[1].split('\\n')[0].strip().strip('\\'')\n        print('main_version version:', main_version)\n\n    version = None\n    pypi_version = None\n    for i in range(5):\n        try:\n            # Use wget to retrieve the PyPI version information\n            subprocess.run(['wget', '-q', '-O', 'pypi_version.json', 'https://pypi.org/pypi/PyPtt/json'], check=True)\n            with open('pypi_version.json', 'r', encoding='utf-8') as f:\n                pypi_data = f.read()\n            pypi_version = pypi_data.split('\"version\":')[1].split('\"')[1]\n            if pypi_version.startswith(main_version):\n                min_pypi_version = pypi_version.split('.')[-1]\n                # the next version\n                version = f\"{main_version}.{int(min_pypi_version) + 1}\"\n            else:\n                version = f\"{main_version}.0\"\n            break\n        except subprocess.CalledProcessError:\n            time.sleep(1)\n\n    if version is None or pypi_version is None:\n        raise ValueError('Can not get version from pypi')\n\n    if not is_merged:\n        commit_file = '/tmp/commit_hash.txt'\n        if os.path.exists(commit_file):\n            with open(commit_file, 'r', encoding='utf-8') as f:\n                commit_hash = f.read().strip()\n        else:\n            max_hash_length = 5\n            try:\n                commit_hash = subprocess.check_output(['git', 'rev-parse', '--long', 'HEAD']).decode('utf-8').strip()\n            except subprocess.CalledProcessError:\n                commit_hash = '0' * max_hash_length\n\n            commit_hash = ''.join([x for x in list(commit_hash) if x.isdigit()])\n\n            if len(commit_hash) < max_hash_length:\n                commit_hash = commit_hash + '0' * (max_hash_length - len(commit_hash))\n            commit_hash = commit_hash[:max_hash_length]\n\n            with open(commit_file, 'w', encoding='utf-8') as f:\n                f.write(commit_hash)\n\n        version = f\"{version}.dev{commit_hash}\"\n\n    if '__version__' in data:\n        current_version = data.split('__version__ = ')[1].split('\\n')[0].strip().strip('\\'')\n        data = data.replace(f\"__version__ = '{current_version}'\", f\"__version__ = '{version}'\")\n    else:\n        data += f'\\n\\n__version__ = \\'{version}\\''\n\n    with open('PyPtt/__init__.py', 'w', encoding='utf-8') as f:\n        f.write(data)\n        f.write('\\n')\n\n    return version\n"
  },
  {
    "path": "setup.py",
    "content": "import os\nimport subprocess\nimport time\n\nfrom setuptools import setup\n\n\ndef version_automation_script():\n    is_merged = os.environ.get('GITHUB_EVENT_NAME') == 'pull_request'\n    print('is_merged:', is_merged)\n\n    # read the main version from __init__.py\n    with open('PyPtt/__init__.py', 'r', encoding='utf-8') as f:\n        data = f.read().strip()\n        main_version = data.split('_main_version = ')[1].split('\\n')[0].strip().strip('\\'')\n        print('main_version version:', main_version)\n\n    version = None\n    pypi_version = None\n    for i in range(5):\n        try:\n            # Use wget to retrieve the PyPI version information\n            subprocess.run(['wget', '-q', '-O', 'pypi_version.json', 'https://pypi.org/pypi/PyPtt/json'], check=True)\n            with open('pypi_version.json', 'r', encoding='utf-8') as f:\n                pypi_data = f.read()\n            pypi_version = pypi_data.split('\"version\":')[1].split('\"')[1]\n            if pypi_version.startswith(main_version):\n                min_pypi_version = pypi_version.split('.')[-1]\n                # the next version\n                version = f\"{main_version}.{int(min_pypi_version) + 1}\"\n            else:\n                version = f\"{main_version}.0\"\n            break\n        except subprocess.CalledProcessError:\n            time.sleep(1)\n\n    if version is None or pypi_version is None:\n        raise ValueError('Can not get version from pypi')\n\n    if not is_merged:\n        commit_file = '/tmp/commit_hash.txt'\n        if os.path.exists(commit_file):\n            with open(commit_file, 'r', encoding='utf-8') as f:\n                commit_hash = f.read().strip()\n        else:\n            max_hash_length = 5\n            try:\n                commit_hash = subprocess.check_output(['git', 'rev-parse', '--long', 'HEAD']).decode('utf-8').strip()\n            except subprocess.CalledProcessError:\n                commit_hash = '0' * max_hash_length\n\n            commit_hash = ''.join([x for x in list(commit_hash) if x.isdigit()])\n\n            if len(commit_hash) < max_hash_length:\n                commit_hash = commit_hash + '0' * (max_hash_length - len(commit_hash))\n            commit_hash = commit_hash[:max_hash_length]\n\n            with open(commit_file, 'w', encoding='utf-8') as f:\n                f.write(commit_hash)\n\n        version = f\"{version}.dev{commit_hash}\"\n\n    if '__version__' in data:\n        current_version = data.split('__version__ = ')[1].split('\\n')[0].strip().strip('\\'')\n        data = data.replace(f\"__version__ = '{current_version}'\", f\"__version__ = '{version}'\")\n    else:\n        data += f'\\n\\n__version__ = \\'{version}\\''\n\n    with open('PyPtt/__init__.py', 'w', encoding='utf-8') as f:\n        f.write(data)\n        f.write('\\n')\n\n    return version\n\n\nversion = version_automation_script()\nprint('the next version:', version)\n\nsetup(\n    name='PyPtt',  # Required\n    version=version,  # Required\n    description='PyPtt\\ngithub: https://github.com/PyPtt/PyPtt',  # Required\n    long_description=open('README.md', encoding=\"utf-8\").read(),  # Optional\n    long_description_content_type='text/markdown',\n    url='https://pyptt.cc/',  # Optional\n\n    author='CodingMan',  # Optional\n    author_email='pttcodingman@gmail.com',  # Optional\n    # https://pypi.org/classifiers/\n    classifiers=[  # Optional\n\n        'Development Status :: 5 - Production/Stable',\n\n        'Operating System :: OS Independent',\n\n        'Intended Audience :: Developers',\n        'Topic :: Communications :: BBS',\n        'Topic :: Software Development :: Libraries :: Python Modules',\n        'Topic :: Internet',\n\n        'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',\n\n        'Programming Language :: Python :: 3.8',\n        'Programming Language :: Python :: 3.9',\n        'Programming Language :: Python :: 3.10',\n        'Programming Language :: Python :: 3.11',\n        'Programming Language :: Python :: 3.12',\n        'Programming Language :: Python :: 3 :: Only',\n\n        'Natural Language :: Chinese (Traditional)',\n        'Natural Language :: English',\n    ],\n    keywords=['PTT', 'crawler', 'bot', 'library', 'websockets'],  # Optional\n\n    python_requires='>=3.8',\n    packages=['PyPtt'],\n    install_requires=[\n        'progressbar2',\n        'websockets',\n        'uao',\n        'requests',\n        'AutoStrEnum',\n        'PyYAML',\n    ],\n    package_data={\n        'PyPtt': ['lang/*.yaml'],\n    }\n)\n"
  },
  {
    "path": "tests/change_pw.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    ptt_bot.change_pw(ptt_bot._ptt_pw)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/comment.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot):\n    if ptt_bot.host == PyPtt.HOST.PTT1:\n        test_list = [\n            # comment the newest post\n            ('Test', None),\n        ]\n    else:\n        test_list = [\n            # comment the newest post\n            ('Test', None),\n        ]\n\n    for board, post_id in test_list:\n        if post_id is None:\n            newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, board)\n\n            for i in range(100):\n                post_info = ptt_bot.get_post(board, index=newest_index - i)\n\n                # if the post is not deleted, save the post\n                if post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.EXISTS:\n                    break\n\n            print('post_id', post_id)\n        elif isinstance(post_id, int):\n            post_info = ptt_bot.get_post(board, index=post_id, query=True)\n        elif isinstance(post_id, str):\n            post_info = ptt_bot.get_post(board, aid=post_id, query=True)\n\n        print(post_info)\n\n        # comment by index\n        ptt_bot.comment(\n            board=board,\n            comment_type=PyPtt.CommentType.ARROW,\n            content='comment by index',\n            index=post_info['index'],\n        )\n\n        # comment by aid\n        ptt_bot.comment(\n            board=board,\n            comment_type=PyPtt.CommentType.ARROW,\n            content='comment by aid',\n            aid=post_info['aid'],\n        )\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n    # assert (result[0] == result[1])\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/config.py",
    "content": "import os\n\n#\nPTT1_ID = os.environ['PTT1_ID']\nPTT1_PW = os.environ['PTT1_PW']\n\nPTT2_ID = os.environ['PTT2_ID']\nPTT2_PW = os.environ['PTT2_PW']\n"
  },
  {
    "path": "tests/exceptions.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\nimport PyPtt\n\nif __name__ == '__main__':\n    try:\n        raise PyPtt.NoPermission('test')\n    except PyPtt.Error as e:\n        print(e.__class__.__name__)\n"
  },
  {
    "path": "tests/get_board_info.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    test_board = [\n        'SYSOP',\n    ]\n\n    for board in test_board:\n        result = ptt_bot.get_board_info(board)\n        log.logger.info('get board info result', result)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/get_board_list.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport json\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    board_list = ptt_bot.get_all_boards()\n\n    with open(f'tests/{ptt_bot.host}-board_list.json', 'w') as f:\n        json.dump(board_list, f, indent=4, ensure_ascii=False)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/get_bottom_post_list.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    result = ptt_bot.get_bottom_post_list('Test')\n\n    print(result)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/get_favourite_boards.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    result = ptt_bot.get_favourite_boards()\n\n    for r in result:\n        print(r)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/get_mail.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    result = []\n    for _ in range(3):\n        mail_index = ptt_bot.get_newest_index(PyPtt.NewIndex.MAIL)\n        mail_info = ptt_bot.get_mail(mail_index)\n\n        log.logger.info('mail result', mail_info)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/get_newest_index.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test_board_index(ptt_bot: PyPtt.API):\n    if ptt_bot.host == PyPtt.HOST.PTT1:\n        test_list = [\n            ('Python', PyPtt.SearchType.KEYWORD, '[公告]'),\n            ('ALLPOST', PyPtt.SearchType.KEYWORD, '(Wanted)'),\n            ('Wanted', PyPtt.SearchType.KEYWORD, '(本文已被刪除)'),\n            ('ALLPOST', PyPtt.SearchType.KEYWORD, '(Gossiping)'),\n            ('Gossiping', PyPtt.SearchType.KEYWORD, '普悠瑪'),\n            ('book', PyPtt.SearchType.KEYWORD, 'AWS'),\n        ]\n    else:\n        test_list = [\n            ('PttSuggest', PyPtt.SearchType.KEYWORD, '[問題]'),\n            # ('PttSuggest', PyPtt.SearchType.COMMENT, '10'),\n        ]\n\n    for board, search_type, search_condition in test_list:\n        for _ in range(3):\n            index = ptt_bot.get_newest_index(\n                PyPtt.NewIndex.BOARD,\n                board)\n            log.logger.info(f'{board} newest index', index)\n\n            index = ptt_bot.get_newest_index(\n                PyPtt.NewIndex.BOARD,\n                board=board,\n                search_type=search_type,\n                search_condition=search_condition)\n            log.logger.info(f'{board} newest index with search', index)\n\n\ndef test_mail_index(ptt_bot: PyPtt.API):\n    for _ in range(3):\n        index = ptt_bot.get_newest_index(\n            PyPtt.NewIndex.MAIL)\n        log.logger.info('mail newest index', index)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        try:\n            util.login(ptt_bot)\n            test_mail_index(ptt_bot)\n            test_mail_index(ptt_bot)\n            test_board_index(ptt_bot)\n            test_board_index(ptt_bot)\n            test_mail_index(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/get_post.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport json\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test_no_condition(ptt_bot: PyPtt.API):\n    result = []\n\n    if ptt_bot.host == PyPtt.HOST.PTT1:\n        test_post_list = [\n            ('Python', 1),\n            # ('NotExitBoard', 1),\n            ('Python', '1TJH_XY0'),\n            # ('Python', '1TJdL7L8'),\n            # # 文章格式錯誤\n            # ('Stock', '1TVnEivO'),\n            # # 文章格式錯誤\n            # ('movie', 457),\n            # ('Gossiping', '1UDnXefr'),\n            # ('joke', '1Tc6G9eQ'),\n            # # 135193\n            # ('Test', 575),\n            # # 待證文章\n            # ('Test', '1U3pLzi0'),\n            # # 古早文章\n            # ('LAW', 1),\n            # # 辦刪除文章\n            # ('Test', 347),\n            # # comment number parse error\n            # ('Ptt25sign', '1VppdKLW'),\n        ]\n    else:\n        test_post_list = [\n            ('WhoAmI', 1),\n        ]\n\n    for board, index in test_post_list:\n        if isinstance(index, int):\n            post = ptt_bot.get_post(\n                board,\n                index=index)\n\n            ptt_bot.get_post(\n                board,\n                index=index,\n                query=True)\n        else:\n            post = ptt_bot.get_post(\n                board,\n                aid=index)\n\n            ptt_bot.get_post(\n                board,\n                aid=index,\n                query=True)\n\n        result.append(post)\n        # util.log.py.info('+==+' * 10)\n        # util.log.py.info(post[PyPtt.PostField.content])\n\n    return result\n\n\ndef get_post_with_condition(ptt_bot: PyPtt.API):\n    def show_condition(test_board, search_type, condition):\n        if search_type == PyPtt.SearchType.KEYWORD:\n            type_str = '關鍵字'\n        if search_type == PyPtt.SearchType.AUTHOR:\n            type_str = '作者'\n        if search_type == PyPtt.SearchType.COMMENT:\n            type_str = '推文數'\n        if search_type == PyPtt.SearchType.MARK:\n            type_str = '標記'\n        if search_type == PyPtt.SearchType.MONEY:\n            type_str = '稿酬'\n\n        log.logger.info(f'{test_board} 使用 {type_str} 搜尋 {condition}')\n\n    if ptt_bot.config.host == PyPtt.HOST.PTT1:\n        test_list = [\n            ('Python', PyPtt.SearchType.KEYWORD, '[公告]'),\n            ('ALLPOST', PyPtt.SearchType.KEYWORD, '(Wanted)'),\n            ('Wanted', PyPtt.SearchType.KEYWORD, '(本文已被刪除)'),\n            ('ALLPOST', PyPtt.SearchType.KEYWORD, '(Gossiping)'),\n            ('Gossiping', PyPtt.SearchType.KEYWORD, '普悠瑪'),\n        ]\n    else:\n        test_list = [\n            ('PttSuggest', PyPtt.SearchType.KEYWORD, '[問題]'),\n            ('PttSuggest', PyPtt.SearchType.COMMENT, '10'),\n        ]\n\n    result = []\n\n    test_range = 1\n    query = False\n\n    for (board, search_type, condition) in test_list:\n        show_condition(board, search_type, condition)\n        index = ptt_bot.get_newest_index(\n            PyPtt.NewIndex.BOARD,\n            board,\n            search_type=search_type,\n            search_condition=condition)\n        util.logger.info(f'{board} 最新文章編號 {index}')\n\n        for i in range(test_range):\n            post = ptt_bot.get_post(\n                board,\n                index=index - i,\n                # PostIndex=611,\n                search_type=search_type,\n                search_condition=condition,\n                query=query)\n\n            # print(json.dumps(post, indent=4))\n\n            log.logger.info('列表日期', post.get('list_date'))\n            log.logger.info('作者', post.get('author'))\n            log.logger.info('標題', post.get('title'))\n\n            if post.get('post_status') == PyPtt.PostStatus.EXISTS:\n                pass\n                # if not query:\n                #     util.log.py.info('內文', post.get('content'))\n            elif post.get('post_status') == PyPtt.PostStatus.DELETED_BY_AUTHOR:\n                log.logger.info('文章被作者刪除')\n            elif post.get('post_status') == PyPtt.PostStatus.DELETED_BY_MODERATOR:\n                log.logger.info('文章被版主刪除')\n            log.logger.info('=' * 50)\n\n            result.append(post)\n\n    return result\n\n\ndef test(ptt_bot: PyPtt.API):\n    result = test_no_condition(ptt_bot)\n\n    print(result)\n    log.logger.info(json.dumps(result, indent=4, ensure_ascii=False))\n\n    # result = get_post_with_condition(ptt_bot)\n    # util.log.py.info(json.dumps(result, ensure_ascii=False, indent=4))\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/get_time.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    result = []\n    for _ in range(10):\n        result.append(ptt_bot.get_time())\n        # time.sleep(1)\n\n    log.logger.info('get time result', result)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/get_user.py",
    "content": "import json\nimport os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    test_users = [\n        'CodingMan',\n    ]\n\n    for test_user in test_users:\n        user_info = ptt_bot.get_user(test_user)\n\n        print(json.dumps(user_info, indent=4, ensure_ascii=False))\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/give_p.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    ptt_bot.give_money('janice001', 10)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        # PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/i18n.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import i18n\nfrom PyPtt import log\n\n\ndef test():\n    PyPtt.i18n.init(PyPtt.Language.ENGLISH)\n\n    print(PyPtt.i18n.goodbye)\n\n    logger = log.init(PyPtt.LogLevel.INFO, 'test')\n    logger.info(\n        i18n.replace(i18n.welcome, 'test version'))\n\n\nif __name__ == '__main__':\n    test()\n"
  },
  {
    "path": "tests/init.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\n\n\ndef test():\n    print('=== default ===')\n    PyPtt.API()\n    print('=== 中文顯示 ===')\n    PyPtt.API(language=PyPtt.Language.MANDARIN)\n    print('=== 英文顯示 ===')\n    PyPtt.API(language=PyPtt.Language.ENGLISH)\n    print('=== log DEBUG ===')\n    PyPtt.API(log_level=PyPtt.LogLevel.DEBUG)\n    print('=== log INFO ===')\n    PyPtt.API(log_level=PyPtt.LogLevel.INFO)\n    print('=== log SILENT===')\n    PyPtt.API(log_level=PyPtt.LogLevel.SILENT)\n\n    print('=== set host with PTT ===')\n    ptt_bot = PyPtt.API(host=PyPtt.HOST.PTT1)\n    print(f'host result {ptt_bot.host}')\n\n    print('=== set host with PTT2 ===')\n    ptt_bot = PyPtt.API(host=PyPtt.HOST.PTT2)\n    print(f'host result {ptt_bot.host}')\n\n    print('=== set host with PTT and TELNET ===')\n    try:\n        PyPtt.API(host=PyPtt.HOST.PTT1, connect_mode=PyPtt.ConnectMode.TELNET)\n        assert False\n    except ValueError:\n        print('通過')\n\n    print('=== set host with PTT2 and TELNET ===')\n    try:\n        PyPtt.API(host=PyPtt.HOST.PTT2, connect_mode=PyPtt.ConnectMode.TELNET)\n        assert False\n    except ValueError:\n        print('通過')\n\n    try:\n        print('=== 語言 99 ===')\n        PyPtt.API(language=99)\n    except TypeError:\n        print('通過')\n    except:\n        print('沒通過')\n        assert False\n    print('=== 語言放字串 ===')\n    try:\n        PyPtt.API(language='PyPtt.i18n.language.ENGLISH')\n    except TypeError:\n        print('通過')\n    except:\n        print('沒通過')\n        assert False\n\n    print('complete')\n\n\nif __name__ == '__main__':\n    test()\n    # PyPtt.API()\n"
  },
  {
    "path": "tests/logger.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\nfrom PyPtt import log\n\n\ndef func():\n    logger = log.init(log.INFO)\n\n    logger.info('1')\n    logger.info('1', '2')\n    logger.info('1', '2', '3')\n\n    logger.debug('debug 1')\n    logger.debug('1', '2')\n    logger.debug('1', '2', '3')\n\n    logger = log.init(log.DEBUG)\n\n    logger.info('234')\n    logger.info('1', '2')\n    logger.info('1', '2', '3')\n\n    logger.debug('debug 2')\n    logger.debug('1', '2')\n    logger.debug('1', '2', '3')\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/login_logout.py",
    "content": "import os\nimport sys\nimport time\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    util.login(ptt_bot, kick=True)\n    ptt_bot.logout()\n\n    print('wait', end=' ')\n\n    max_wait_time = 5\n    for sec in range(max_wait_time):\n        print(max_wait_time - sec, end=' ')\n        time.sleep(1)\n    print()\n\n    util.login(ptt_bot, kick=False)\n    ptt_bot.logout()\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        test(ptt_bot)\n\n    print('login logout test ok')\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/performance.py",
    "content": "import os\nimport sys\nimport time\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot):\n    test_time = 500\n    print(f'效能測試 get_time {test_time} 次')\n\n    start_time = time.time()\n    for _ in range(test_time):\n        ptt_time = ptt_bot.get_time()\n\n        assert ptt_time is not None\n\n    end_time = time.time()\n    print(\n        F'Performance Test get_time {end_time - start_time} s')\n\n    print('Performance Test finish')\n\n\ndef func():\n    ptt_bot_list = [\n        PyPtt.API()]\n\n    for ptt_bot in ptt_bot_list:\n        util.login(ptt_bot)\n\n        test(ptt_bot)\n\n        ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/post.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport time\n\nimport PyPtt\n\nfrom PyPtt import PostField\nfrom tests import util\nfrom PyPtt import log\n\n\ndef test(ptt_bot: PyPtt.API):\n    content = '''\n此為 PyPtt 貼文測試內容，如有打擾請告知。\n官方網站: https://pyptt.cc\n\n測試標記\n781d16268c9f25a39142a17ff063ac029b1466ca14cb34f5d88fe8aadfeee053\n'''\n\n    temp = ''\n    for i in range(100):\n        content = f'{content}\\n={i}='\n        temp = f'{temp}\\n={i}='\n\n    check_ = [\n        '781d16268c9f25a39142a17ff063ac029b1466ca14cb34f5d88fe8aadfeee053',\n        temp\n    ]\n\n    check_range = 3\n\n    for _ in range(check_range):\n        ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content=content, sign_file=0)\n\n    time.sleep(1)\n\n    newest_index = ptt_bot.get_newest_index(index_type=PyPtt.NewIndex.BOARD, board='Test')\n\n    # find post what we post\n    post_list = []\n    for i in range(10):\n        post = ptt_bot.get_post(board='Test', index=newest_index - i)\n\n        if post[PostField.post_status] != PyPtt.PostStatus.EXISTS:\n            print(f'Post {newest_index - i} not exists')\n            continue\n\n        post_author = post[PostField.author]\n        post_author = post_author.split(' ')[0]\n        if post_author != ptt_bot.ptt_id:\n            print(f'Post {newest_index - i} author not match', post_author)\n            continue\n\n        post_list.append(newest_index - i)\n        if len(post_list) == check_range:\n            break\n\n    comment_check = []\n    for index in post_list:\n        for i in range(5):\n            comment_check.append(f'={i}=')\n            ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.ARROW, content=f'={i}=', index=index)\n    comment_check = list(set(comment_check))\n\n    time.sleep(1)\n\n    for i, index in enumerate(post_list):\n\n        log.logger.info('test', i)\n\n        post = ptt_bot.get_post(board='Test', index=index)\n\n        if post[PostField.post_status] != PyPtt.PostStatus.EXISTS:\n            log.logger.info('fail')\n            print(f'Post {index} not exists')\n            break\n\n        post_author = post[PostField.author]\n        post_author = post_author.split(' ')[0]\n        if post_author != ptt_bot.ptt_id:\n            log.logger.info('fail')\n            print(f'Post {index} author not match', post_author)\n            break\n\n        check = True\n        for c in check_:\n            if c not in post[PostField.content]:\n                check = False\n                break\n        if not check:\n            log.logger.info('fail')\n            print(f'Post {index} content not match')\n            break\n\n        cur_comment_check = set()\n        for comment in post[PostField.comments]:\n\n            if comment[PyPtt.CommentField.content] in comment_check:\n                cur_comment_check.add(comment[PyPtt.CommentField.content])\n            else:\n                log.logger.info('comment', comment[PyPtt.CommentField.content])\n\n        if len(cur_comment_check) != len(comment_check):\n            log.logger.info('fail')\n            print(f'Post {index} comment not match')\n            break\n\n        log.logger.info('pass')\n\n    # for index in post_list:\n    #     ptt_bot.del_post(board='Test', index=index)\n\n    util.del_all_post(ptt_bot)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        util.login(ptt_bot)\n\n        test(ptt_bot)\n\n        ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/reply.py",
    "content": "import os\nimport sys\nimport time\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom PyPtt import PostField\nfrom tests import util\n\ncurrent_id = None\n\n\ndef test(ptt_bot: PyPtt.API):\n    ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content='測試內文', sign_file=0)\n\n    time.sleep(1)\n\n    newest_index = ptt_bot.get_newest_index(index_type=PyPtt.NewIndex.BOARD, board='Test')\n\n    for i in range(5):\n        cur_post = ptt_bot.get_post(board='Test', index=newest_index - i)\n\n        if cur_post[PostField.post_status] != PyPtt.PostStatus.EXISTS:\n            continue\n\n        cur_author = cur_post[PostField.author]\n        cur_author = cur_author.split(' ')[0]\n        if cur_author.lower() != ptt_bot.ptt_id.lower():\n            continue\n\n        ptt_bot.reply_post(\n            reply_to=PyPtt.ReplyTo.BOARD, board='Test', index=newest_index - i, content='PyPtt 程式回覆測試')\n\n        break\n    newest_index += 1\n\n    time.sleep(1)\n\n    posts = []\n\n    # 在十篇範圍內找尋我們的文章\n    for i in range(10):\n        cur_post = ptt_bot.get_post(board='Test', index=newest_index - i)\n\n        if cur_post[PostField.post_status] != PyPtt.PostStatus.EXISTS:\n            continue\n\n        cur_author = cur_post[PostField.author]\n        cur_author = cur_author.split(' ')[0]\n        if cur_author.lower() != ptt_bot.ptt_id.lower():\n            continue\n\n        posts.append(cur_post[PostField.aid])\n\n    log.logger.info('test')\n    if len(posts) < 2:\n        log.logger.info('len(posts) < 2, fail')\n        return\n\n    check = [\n        '[測試] PyPtt 程式貼文測試',\n        'Re: [測試] PyPtt 程式貼文測試'\n    ]\n\n    check_result = True\n    for aid in posts:\n        post = ptt_bot.get_post(board='Test', aid=aid)\n\n        if post[PostField.post_status] != PyPtt.PostStatus.EXISTS:\n            log.logger.info('post[PostField.post_status] != PyPtt.PostStatus.EXISTS, fail')\n            check_result = False\n            break\n\n        if post[PostField.title] not in check:\n            log.logger.info('post[PostField.title] not in check, fail')\n            check_result = False\n            break\n\n        check.remove(post[PostField.title])\n\n    if check_result:\n        log.logger.info('pass')\n    else:\n        log.logger.info('fail')\n\n    util.del_all_post(ptt_bot)\n\n\ndef func():\n    global current_id\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/search_user.py",
    "content": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n    test_users = [\n        'Coding',\n    ]\n\n    for test_user in test_users:\n        result = ptt_bot.search_user(test_user)\n\n        log.logger.info('result', result)\n\n\ndef func():\n    host_list = [\n        PyPtt.HOST.PTT1,\n        PyPtt.HOST.PTT2\n    ]\n\n    for host in host_list:\n        ptt_bot = PyPtt.API(\n            host=host,\n            # log_level=PyPtt.LogLevel.DEBUG,\n        )\n        try:\n            util.login(ptt_bot)\n            test(ptt_bot)\n        finally:\n            ptt_bot.logout()\n\n\nif __name__ == '__main__':\n    func()\n"
  },
  {
    "path": "tests/service.py",
    "content": "import os\nimport sys\nimport threading\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import Service\nfrom tests import config\n\n\ndef api_test(thread_id, service):\n    result = service.call('get_time')\n    print(f'thread id {thread_id}', 'get_time', result)\n\n    result = service.call('get_aid_from_url', {'url': 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html'})\n    print(f'thread id {thread_id}', 'get_aid_from_url', result)\n\n    result = service.call('get_newest_index', {'index_type': PyPtt.NewIndex.BOARD, 'board': 'Python'})\n    print(f'thread id {thread_id}', 'get_newest_index', result)\n\n\ndef test():\n    pyptt_init_config = {\n        # 'language': PyPtt.Language.ENGLISH,\n    }\n\n    service = Service(pyptt_init_config)\n\n    try:\n\n        service.call('login', {'ptt_id': config.PTT1_ID, 'ptt_pw': config.PTT1_PW})\n\n        pool = []\n        for i in range(10):\n            t = threading.Thread(target=api_test, args=(i, service))\n            t.start()\n            pool.append(t)\n\n        for t in pool:\n            t.join()\n\n        service.call('logout')\n    finally:\n        service.close()\n\n\nif __name__ == '__main__':\n    test()\n    # pass\n"
  },
  {
    "path": "tests/util.py",
    "content": "import json\n\nimport PyPtt\nfrom PyPtt import PostField\nfrom PyPtt import log\nfrom . import config\n\n\ndef log_to_file(msg: str):\n    with open('single_log.txt', 'a', encoding='utf8') as f:\n        f.write(f'{msg}\\n')\n\n\ndef get_id_pw(password_file):\n    try:\n        with open(password_file) as AccountFile:\n            account = json.load(AccountFile)\n            ptt_id = account['id']\n            password = account['pw']\n    except FileNotFoundError:\n        print(f'Please write PTT ID and Password in {password_file}')\n        print('{\"id\":\"your ptt id\", \"pw\":\"your ptt pw\"}')\n        assert False\n\n    return ptt_id, password\n\n\ndef login(ptt_bot: PyPtt.API, kick: bool = True):\n    if ptt_bot.host == PyPtt.HOST.PTT1:\n        ptt_id, ptt_pw = config.PTT1_ID, config.PTT1_PW\n    else:\n        ptt_id, ptt_pw = config.PTT2_ID, config.PTT2_PW\n\n    for _ in range(3):\n        try:\n            ptt_bot.login(ptt_id=ptt_id, ptt_pw=ptt_pw, kick_other_session=kick)\n            break\n        except PyPtt.LoginError:\n            log.logger.info('登入失敗')\n            assert False\n        except PyPtt.WrongIDorPassword:\n            log.logger.info('帳號密碼錯誤')\n            assert False\n        except PyPtt.LoginTooOften:\n            log.logger.info('請稍等一下再登入')\n            assert False\n\n    if not ptt_bot.is_registered_user:\n        log.logger.info('未註冊使用者')\n\n        if ptt_bot.process_picks != 0:\n            log.logger.info(f'註冊單處理順位 {ptt_bot.process_picks}')\n\n\ndef show_data(data, key: str = None):\n    if isinstance(data, dict):\n        log.logger.info(f'{key}: {data[key]}')\n\n\ndef del_all_post(ptt_bot: PyPtt.API):\n    newest_index = ptt_bot.get_newest_index(index_type=PyPtt.NewIndex.BOARD, board='Test')\n\n    for i in range(30):\n        try:\n            ptt_bot.del_post(board='Test', index=newest_index - i)\n        except:\n            pass\n"
  },
  {
    "path": "upload.sh",
    "content": "#!/bin/sh\n\necho PyPtt uploader v 1.0.2\n\nrm -r dist build\npython3 setup.py sdist bdist_wheel --universal\n\ncase $1 in\nrelease)\n    echo upload to pypi\n    python3 -m twine upload dist/*\n    ;;\ntest)\n    echo upload to testpypi\n    python3 -m twine upload --repository testpypi dist/*\n    ;;\n*)\n    echo \"unknown command [$@]\"\n    ;;\nesac\n\necho Upload finish"
  }
]