Full Code of PttCodingMan/PyPtt for AI

master f277f1adf595 cached
119 files
328.8 KB
106.0k tokens
299 symbols
1 requests
Download .txt
Showing preview only (373K chars total). Download the full file or copy to clipboard to get everything.
Repository: PttCodingMan/PyPtt
Branch: master
Commit: f277f1adf595
Files: 119
Total size: 328.8 KB

Directory structure:
gitextract_7t96yf9r/

├── .github/
│   └── workflows/
│       ├── deploy.yml
│       ├── docs.yml
│       └── test.yml
├── .gitignore
├── GourceScript.bat
├── LICENSE
├── MANIFEST.in
├── PyPtt/
│   ├── PTT.py
│   ├── __init__.py
│   ├── _api_bucket.py
│   ├── _api_call_status.py
│   ├── _api_change_pw.py
│   ├── _api_comment.py
│   ├── _api_del_post.py
│   ├── _api_get_board_info.py
│   ├── _api_get_board_list.py
│   ├── _api_get_bottom_post_list.py
│   ├── _api_get_favourite_board.py
│   ├── _api_get_newest_index.py
│   ├── _api_get_post.py
│   ├── _api_get_post_index.py
│   ├── _api_get_time.py
│   ├── _api_get_user.py
│   ├── _api_give_money.py
│   ├── _api_has_new_mail.py
│   ├── _api_loginout.py
│   ├── _api_mail.py
│   ├── _api_mark_post.py
│   ├── _api_post.py
│   ├── _api_reply_post.py
│   ├── _api_search_user.py
│   ├── _api_set_board_title.py
│   ├── _api_util.py
│   ├── check_value.py
│   ├── command.py
│   ├── config.py
│   ├── connect_core.py
│   ├── data_type.py
│   ├── exceptions.py
│   ├── i18n.py
│   ├── lang/
│   │   ├── en_US.yaml
│   │   └── zh_TW.yaml
│   ├── lib_util.py
│   ├── log.py
│   ├── screens.py
│   └── service.py
├── README.md
├── docs/
│   ├── CNAME
│   ├── Makefile
│   ├── api/
│   │   ├── bucket.rst
│   │   ├── change_pw.rst
│   │   ├── comment.rst
│   │   ├── del_mail.rst
│   │   ├── del_post.rst
│   │   ├── get_aid_from_url.rst
│   │   ├── get_all_boards.rst
│   │   ├── get_board_info.rst
│   │   ├── get_bottom_post_list.rst
│   │   ├── get_favourite_boards.rst
│   │   ├── get_mail.rst
│   │   ├── get_newest_index.rst
│   │   ├── get_post.rst
│   │   ├── get_time.rst
│   │   ├── get_user.rst
│   │   ├── give_money.rst
│   │   ├── index.rst
│   │   ├── init.rst
│   │   ├── login_logout.rst
│   │   ├── mail.rst
│   │   ├── mark_post.rst
│   │   ├── post.rst
│   │   ├── reply_post.rst
│   │   ├── search_user.rst
│   │   └── set_board_title.rst
│   ├── changelog.rst
│   ├── conf.py
│   ├── dev.rst
│   ├── docker.rst
│   ├── examples.rst
│   ├── exceptions.rst
│   ├── faq.rst
│   ├── index.rst
│   ├── install.rst
│   ├── make.bat
│   ├── requirements.txt
│   ├── roadmap.rst
│   ├── robots.txt
│   ├── service.rst
│   └── type.rst
├── make_doc.sh
├── requirements.txt
├── scripts/
│   ├── lang.py
│   └── package_script.py
├── setup.py
├── tests/
│   ├── change_pw.py
│   ├── comment.py
│   ├── config.py
│   ├── exceptions.py
│   ├── get_board_info.py
│   ├── get_board_list.py
│   ├── get_bottom_post_list.py
│   ├── get_favourite_boards.py
│   ├── get_mail.py
│   ├── get_newest_index.py
│   ├── get_post.py
│   ├── get_time.py
│   ├── get_user.py
│   ├── give_p.py
│   ├── i18n.py
│   ├── init.py
│   ├── logger.py
│   ├── login_logout.py
│   ├── performance.py
│   ├── post.py
│   ├── reply.py
│   ├── search_user.py
│   ├── service.py
│   └── util.py
└── upload.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/deploy.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: deploy

# run on merge to master or manual trigger
on:
  pull_request:
    types: [ closed ]
    branches:
      - master
    paths:
      - 'PyPtt/*.py'
      - 'setup.py'
  workflow_dispatch:

jobs:
  deploy:
    name: Deploy to PyPI and Docker
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install build
      - name: Build package
        run: |
          python -m build
      - name: Publish package to TestPyPI
        if: github.event_name == 'workflow_dispatch' && github.event.pull_request.merged == false
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          password: ${{ secrets.TEST_PYPI_API_TOKEN }}
          repository_url: https://test.pypi.org/legacy/
      - name: Publish package
        if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          password: ${{ secrets.PYPI_API_TOKEN }}
      - name: Trigger Docker build
        if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true
        uses: InformaticsMatters/trigger-ci-action@1.0.1
        with:
          ci-owner: PyPtt
          ci-repository: PyPtt_image
          ci-user: PttCodingMan
          ci-ref: refs/heads/main
          ci-user-token: ${{ secrets.ACCESS_TOKEN }}
          ci-name: build PyPtt image


================================================
FILE: .github/workflows/docs.yml
================================================

name: docs

# run on merge to master or manual trigger
on:
  pull_request:
    types: [ closed ]
    paths:
      - 'docs/**/*'
      - 'PyPtt/*.py'
  workflow_dispatch:

jobs:
  build:
    if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true
    name: Build doc and Deploy
    runs-on: ubuntu-latest
    steps:
    - uses: actions/setup-python@v5
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
    - name: Build and Commit
      uses: sphinx-notes/pages@v2
      with:
        requirements_path: docs/requirements.txt
    - name: Push changes
      uses: ad-m/github-push-action@master
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        branch: gh-pages


================================================
FILE: .github/workflows/test.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: test

# run on every PR creation, or manual trigger
# run on every push to non-master branch
on:
  pull_request:
    types:
      - opened
  push:
    branches-ignore:
      - 'master'
  workflow_dispatch:
  
env:
  DEP_PATH: requirements.txt

jobs:
  check:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]

    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install flake8 pytest
          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
      - name: Lint with flake8
        run: |
          # stop the build if there are Python syntax errors or undefined names
          flake8 PyPtt/ --count --select=E9,F63,F7,F82 --show-source --statistics
          # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
          flake8 PyPtt/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
      - name: Test
        run: |
          python tests/init.py
  scan:
    runs-on: ubuntu-latest
    steps:
      - name: Check out master
        uses: actions/checkout@v4

      - name: detect-secrets
        uses: reviewdog/action-detect-secrets@master
        with:
          reporter: github-pr-review

      - name: Security vulnerabilities scan
        uses: aufdenpunkt/python-safety-check@master
        with:
          scan_requirements_file_only: true
    


================================================
FILE: .gitignore
================================================
__pycache__/
build/
dist/
/CrawlBoardResult.txt
/.pypirc
PTTLibrary.egg-info/
*Out.txt
/Big5Data.txt
PTTLibrary-*/
/PTTLibrary/i18n.txt
/Test.txt
Account*.txt
*.spec
/LogHandler.txt
.vscode
.idea/
venv/
/log.txt
PyPtt.egg-info/
/test_account*.txt
/test_result.txt
*.json
tests/ptt.sh
docs/_build/
.DS_Store
test*.py
ptt.sh


================================================
FILE: GourceScript.bat
================================================
@echo off
cls

gource --seconds-per-day 0.05 --title "PTT Library"

================================================
FILE: LICENSE
================================================
                   GNU LESSER GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.


  This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.

  0. Additional Definitions.

  As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.

  "The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.

  An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.

  A "Combined Work" is a work produced by combining or linking an
Application with the Library.  The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".

  The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.

  The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.

  1. Exception to Section 3 of the GNU GPL.

  You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.

  2. Conveying Modified Versions.

  If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:

   a) under this License, provided that you make a good faith effort to
   ensure that, in the event an Application does not supply the
   function or data, the facility still operates, and performs
   whatever part of its purpose remains meaningful, or

   b) under the GNU GPL, with none of the additional permissions of
   this License applicable to that copy.

  3. Object Code Incorporating Material from Library Header Files.

  The object code form of an Application may incorporate material from
a header file that is part of the Library.  You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:

   a) Give prominent notice with each copy of the object code that the
   Library is used in it and that the Library and its use are
   covered by this License.

   b) Accompany the object code with a copy of the GNU GPL and this license
   document.

  4. Combined Works.

  You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:

   a) Give prominent notice with each copy of the Combined Work that
   the Library is used in it and that the Library and its use are
   covered by this License.

   b) Accompany the Combined Work with a copy of the GNU GPL and this license
   document.

   c) For a Combined Work that displays copyright notices during
   execution, include the copyright notice for the Library among
   these notices, as well as a reference directing the user to the
   copies of the GNU GPL and this license document.

   d) Do one of the following:

       0) Convey the Minimal Corresponding Source under the terms of this
       License, and the Corresponding Application Code in a form
       suitable for, and under terms that permit, the user to
       recombine or relink the Application with a modified version of
       the Linked Version to produce a modified Combined Work, in the
       manner specified by section 6 of the GNU GPL for conveying
       Corresponding Source.

       1) Use a suitable shared library mechanism for linking with the
       Library.  A suitable mechanism is one that (a) uses at run time
       a copy of the Library already present on the user's computer
       system, and (b) will operate properly with a modified version
       of the Library that is interface-compatible with the Linked
       Version.

   e) Provide Installation Information, but only if you would otherwise
   be required to provide such information under section 6 of the
   GNU GPL, and only to the extent that such information is
   necessary to install and execute a modified version of the
   Combined Work produced by recombining or relinking the
   Application with a modified version of the Linked Version. (If
   you use option 4d0, the Installation Information must accompany
   the Minimal Corresponding Source and Corresponding Application
   Code. If you use option 4d1, you must provide the Installation
   Information in the manner specified by section 6 of the GNU GPL
   for conveying Corresponding Source.)

  5. Combined Libraries.

  You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:

   a) Accompany the combined library with a copy of the same work based
   on the Library, uncombined with any other library facilities,
   conveyed under the terms of this License.

   b) Give prominent notice with the combined library that part of it
   is a work based on the Library, and explaining where to find the
   accompanying uncombined form of the same work.

  6. Revised Versions of the GNU Lesser General Public License.

  The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.

  Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.

  If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.


================================================
FILE: MANIFEST.in
================================================
include LICENSE
include README.md

# Include the data files
recursive-include PyPtt *

================================================
FILE: PyPtt/PTT.py
================================================
from __future__ import annotations

import functools
import threading
from typing import Dict, Tuple, Callable, List, Optional, Any

from . import __version__
from . import _api_bucket
from . import _api_change_pw
from . import _api_comment
from . import _api_del_post
from . import _api_get_board_info
from . import _api_get_board_list
from . import _api_get_bottom_post_list
from . import _api_get_favourite_board
from . import _api_get_newest_index
from . import _api_get_post
from . import _api_get_time
from . import _api_get_user
from . import _api_give_money
from . import _api_loginout
from . import _api_mail
from . import _api_mark_post
from . import _api_post
from . import _api_reply_post
from . import _api_search_user
from . import _api_set_board_title
from . import check_value
from . import config
from . import connect_core
from . import data_type
from . import i18n
from . import lib_util
from . import log


class API:
    def __init__(self, **kwargs):
        """

        初始化 PyPtt。

        Args:
            language (:ref:`language`): PyPtt 顯示訊息的語言。預設為 **MANDARIN**。
            log_level (LogLevel_): PyPtt 顯示訊息的等級。預設為 **INFO**。
            screen_timeout (int): 經過 screen_timeout 秒之後, PyPtt 將會判定無法判斷目前畫面的狀況。預設為 **3 秒**。
            screen_long_timeout (int): 經過 screen_long_timeout 秒之後,PyPtt 將會判定無法判斷目前畫面的狀況,這會用在較長的等待時間,例如踢掉其他連線等等。預設為 **10 秒**。
            screen_post_timeout (int): 經過 screen_post_timeout 秒之後,PyPtt 將會判定無法判斷目前畫面的狀況,這會用在較長的等待時間,例如發佈文章等等。預設為 **60 秒**。
            connect_mode (:ref:`connect-mode`): PyPtt 連線的模式。預設為 **WEBSOCKETS**。
            logger_callback (Callable): PyPtt 顯示訊息的 callback。預設為 None。
            port (int): PyPtt 連線的 port。預設為 **23**。
            host (:ref:`host`): PyPtt 連線的 PTT 伺服器。預設為 **PTT1**。
            check_update (bool): 是否檢查 PyPtt 的更新。預設為 **True**。

        Returns:
            None

        範例::

            import PyPtt
            ptt_bot = PyPtt.API()

        參考: :ref:`language`、LogLevel_、:ref:`connect-mode`、:ref:`host`

        .. _LogLevel: https://github.com/PttCodingMan/SingleLog/blob/d7c19a1b848dfb1c9df8201f13def9a31afd035c/SingleLog/SingleLog.py#L22

        英文顯示範例::

            import PyPtt

            ptt_bot = PyPtt.API(
                language=PyPtt.Language.ENGLISH)

        除錯範例::

            import PyPtt

            ptt_bot = PyPtt.API(
                log_level=PyPtt.LogLevel.DEBUG)


        """

        log_level = kwargs.get('log_level', log.INFO)
        if not isinstance(log_level, log.LogLv):
            raise TypeError('[PyPtt] log_level must be log.Level')

        logger_callback = kwargs.get('logger_callback', None)
        log.init(log_level, logger_callback=logger_callback)

        language = kwargs.get('language', data_type.Language.MANDARIN)
        if not isinstance(language, str):
            raise TypeError('[PyPtt] language must be PyPtt.Language')
        if language not in i18n.locale_pool:
            raise TypeError('[PyPtt] language must be PyPtt.Language')

        self.config = config.Config()
        self.config.log_level = log_level

        self.config.language = language

        print('language', self.config.language)
        i18n.init(self.config.language)

        self.is_mailbox_full: bool = False
        self.is_registered_user: bool = False
        self.process_picks: int = 0

        self.ptt_id: str = ''
        self._ptt_pw: str = ''
        self._is_login: bool = False

        host = kwargs.get('host', data_type.HOST.PTT1)
        screen_timeout = kwargs.get('screen_timeout', 3.0)
        screen_long_timeout = kwargs.get('screen_long_timeout', 10.0)
        screen_post_timeout = kwargs.get('screen_post_timeout', 60.0)

        check_value.check_type(host, (data_type.HOST, str), 'host')
        check_value.check_type(screen_timeout, float, 'screen_timeout')
        check_value.check_type(screen_long_timeout, float, 'screen_long_timeout')
        check_value.check_type(screen_post_timeout, float, 'screen_post_timeout')

        if screen_timeout != 0:
            self.config.screen_timeout = screen_timeout
        if screen_long_timeout != 0:
            self.config.screen_long_timeout = screen_long_timeout
        if screen_post_timeout != 0:
            self.config.screen_post_timeout = screen_post_timeout

        self.config.host = host
        self.host = host

        port = kwargs.get('port', 23)

        check_value.check_type(port, int, 'port')
        check_value.check_range(port, 1, 65535 - 1, 'port')
        self.config.port = port

        connect_mode = kwargs.get('connect_mode', data_type.ConnectMode.WEBSOCKETS)

        check_value.check_type(connect_mode, data_type.ConnectMode, 'connect_mode')
        if host in [data_type.HOST.PTT1, data_type.HOST.PTT2] and connect_mode is data_type.ConnectMode.TELNET:
            raise ValueError('[PyPtt] TELNET is not available on PTT1 and PTT2')
        self.config.connect_mode = connect_mode

        self.connect_core = connect_core.API(self.config)
        self._exist_board_list = []
        self._moderators = dict()
        self._thread_id = threading.get_ident()
        self._goto_board_list = []
        self._board_info_list = dict()
        self._newest_index_data = data_type.TimedDict(timeout=2)

        log.logger.debug('thread_id', self._thread_id)

        log.logger.info(
            i18n.replace(i18n.welcome, __version__))

        log.logger.info('PyPtt', i18n.initialization)

        if self.config.connect_mode == data_type.ConnectMode.TELNET:
            log.logger.info(i18n.set_connect_mode, '...', i18n.connect_mode_TELNET)
        elif self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:
            log.logger.info(i18n.set_connect_mode, '...', i18n.connect_mode_WEBSOCKET)

        if self.config.language == data_type.Language.MANDARIN:
            log.logger.info(i18n.set_up_lang_module, '...', i18n.mandarin_module)
        elif self.config.language == data_type.Language.ENGLISH:
            log.logger.info(i18n.set_up_lang_module, '...', i18n.english_module)

        if self.config.host == data_type.HOST.PTT1:
            log.logger.info(i18n.set_connect_host, '...', i18n.PTT)
        elif self.config.host == data_type.HOST.PTT2:
            log.logger.info(i18n.set_connect_host, '...', i18n.PTT2)
        elif self.config.host == data_type.HOST.LOCALHOST:
            log.logger.info(i18n.set_connect_host, '...', i18n.localhost)
        else:
            log.logger.info(i18n.set_connect_host, '...', self.config.host)

        log.logger.info('PyPtt', i18n.initialization, '...', i18n.done)

        check_update = kwargs.get('check_update', True)
        check_value.check_type(check_update, bool, 'check_update')

        if check_update:
            version_compare, remote_version = lib_util.sync_version()

            if version_compare is data_type.Compare.SMALLER:
                log.logger.info(i18n.current_version, __version__)
                log.logger.info(i18n.new_version, remote_version)
            elif version_compare is data_type.Compare.BIGGER:
                log.logger.info(i18n.development_version, __version__)
            else:
                log.logger.info(i18n.latest_version, __version__)
        else:
            log.logger.info(i18n.current_version, __version__)

    def __del__(self):
        if log.logger:
            log.logger.debug(i18n.goodbye)

    def login(self, ptt_id: str, ptt_pw: str, kick_other_session: bool = False) -> None:

        """
        登入 PTT。

        Args:
            ptt_id (str): PTT ID。
            ptt_pw (str): PTT 密碼。
            kick_other_session (bool): 是否踢掉其他登入的 session。預設為 False。

        Returns:
            None

        Raises:
            LoginError: 登入失敗。
            WrongIDorPassword: 帳號或密碼錯誤。
            OnlySecureConnection: 只能使用安全連線。
            ResetYourContactEmail: 請先至信箱設定連絡信箱。

        範例::

            import PyPtt
            ptt_bot = PyPtt.API()

            try:
                ptt_bot.login(
                    ptt_id='ptt_id', ptt_pw='ptt_pw', kick_other_session=True)
            except PyPtt.LoginError:
                print('登入失敗')
            except PyPtt.WrongIDorPassword:
                print('帳號密碼錯誤')
            except PyPtt.OnlySecureConnection:
                print('只能使用安全連線')
            except PyPtt.ResetYourContactEmail:
                print('請先至信箱設定連絡信箱')

        """

        _api_loginout.login(self, ptt_id, ptt_pw, kick_other_session)

    def logout(self) -> None:
        """
        登出 PTT。

        Returns:
            None

        範例::

            import PyPtt
            ptt_bot = PyPtt.API()

            try:
                # .. login ..
                # .. do something ..
            finally:
                ptt_bot.logout()

        """

        _api_loginout.logout(self)

    def get_time(self) -> str:

        """
        取得 PTT 系統時間。

        Returns:
            None

        範例::

            import PyPtt
            ptt_bot = PyPtt.API()

            try:
                # .. login ..
                time = ptt_bot.get_time()
                # .. do something ..
            finally:
                ptt_bot.logout()
        """

        return _api_get_time.get_time(self)

    def get_post(self, board: str, aid: Optional[str] = None, index: Optional[int] = None,
                 search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None,
                 search_list: Optional[List[tuple]] = None, query: bool = False) -> Dict:
        """
        取得文章。

        Args:
            board (str): 看板名稱。
            aid (str): 文章編號。
            index: 文章編號。
            search_list (List[str]): 搜尋清單。
            query (bool): 是否為查詢模式。

        Returns:
            Dict,文章內容。詳見 :ref:`post-field`

        Raises:
            RequireLogin: 需要登入。
            NoSuchBoard: 看板不存在。

        使用 AID 範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                post_info = ptt_bot.get_post('Python', aid='1TJH_XY0')
                # .. do something ..
            finally:
                ptt_bot.logout()

        使用 index 範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                post_info = ptt_bot.get_post('Python', index=1)
                # .. do something ..
            finally:
                ptt_bot.logout()

        使用搜尋範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                post_info = ptt_bot.get_post(
                    'Python',
                    index=1,
                    search_list=[(PyPtt.SearchType.KEYWORD, 'PyPtt')]
                )
                # .. do something ..
            finally:
                ptt_bot.logout()

        | 更多範例參考 :ref:`取得文章 <check_post_status>`
        | 參考 :ref:`取得最新文章編號 <api-get-newest-index>`
        """
        return _api_get_post.get_post(
            self, board, aid=aid, index=index, search_type=search_type, search_condition=search_condition,
            search_list=search_list, query=query)

    def get_newest_index(self, index_type: data_type.NewIndex, board: Optional[str] = None,
                         search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None,
                         search_list: Optional[List[Tuple[Any | str]]] = None, ) -> int:
        """
        取得最新文章或信箱編號。

        Args:
            index_type (:ref:`new-index`): 編號類型。
            board (str): 看板名稱。
            search_list (List[str]): 搜尋清單。

        Returns:
            int,最新文章或信箱編號。

        Raises:
            RequireLogin: 需要登入。
            NoSuchBoard: 看板不存在。

        取得最新看板編號::

            import PyPtt

            ptt_bot = PyPtt.API()

            # get newest index of board
            try:
                # .. login ..
                newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, 'Python')
                # .. do something ..
            finally:
                ptt_bot.logout()


        取得最新文章編號使用搜尋::

            import PyPtt

            ptt_bot = PyPtt.API()

            search_list = [(PyPtt.SearchType.KEYWORD, 'PyPtt')]

            try:
                # .. login ..
                newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, 'Python', search_list=search_list)
                # .. do something ..
            finally:
                ptt_bot.logout()

        取得最新信箱編號::

            import PyPtt

            ptt_bot = PyPtt.API()

            # get newest index of mail
            try:
                # .. login ..
                newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.MAIL)
                # .. do something ..
            finally:
                ptt_bot.logout()

        參考 :ref:`搜尋編號種類 <new-index>`、:ref:`取得文章 <api-get-post>`
        """

        return _api_get_newest_index.get_newest_index(
            self, index_type, board, search_type, search_condition, search_list)

    def post(self, board: str, title_index: int, title: str, content: str, sign_file: [str | int] = 0) -> None:
        """
        發文。

        Args:
            board (str): 看板名稱。
            title_index (int): 文章標題編號。
            title (str): 文章標題。
            content (str): 文章內容。
            sign_file  (str | int): 編號或隨機簽名檔 (x),預設為 0 (不選)。

        Returns:
            None

        Raises:
            UnregisteredUser: 未註冊使用者。
            RequireLogin: 需要登入。
            NoSuchBoard: 看板不存在。
            NoPermission: 沒有發佈權限。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content='測試內容', sign_file=0)
                # .. do something ..
            finally:
                ptt_bot.logout()

        """

        _api_post.post(self, board, title, content, title_index, sign_file)

    def comment(self, board: str, comment_type: data_type.CommentType, content: str, aid: Optional[str] = None,
                index: int = 0) -> None:
        """
        推文。

        Args:
            board (str): 看板名稱。
            comment_type (:ref:`comment-type`): 推文類型。
            content (str): 推文內容。
            aid (str): 文章編號。
            index (int): 文章編號。

        Returns:
            None

        Raises:
            UnregisteredUser: 未註冊使用者。
            RequireLogin: 需要登入。
            NoSuchBoard: 看板不存在。
            NoSuchPost: 文章不存在。
            NoPermission: 沒有推文權限。
            NoFastComment: 推文間隔太短。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.PUSH, content='Comment by index', index=123)
                ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.PUSH, content='Comment by index', aid='17MrayxF')
                # .. do something ..
            finally:
                ptt_bot.logout()

        參考 :ref:`推文類型 <comment-type>`、:ref:`取得最新文章編號 <api-get-newest-index>`
        """

        _api_comment.comment(self, board, comment_type, content, aid, index)

    def get_user(self, user_id: str) -> Dict:

        """
        取得使用者資訊。

        Args:
            user_id (str): 使用者 ID。

        Returns:
            Dict,使用者資訊。詳見 :ref:`使用者資料欄位 <user-field>`

        Raises:
            RequireLogin: 需要登入。
            NoSuchUser: 使用者不存在。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                user_info = ptt_bot.get_user('CodingMan')
                # .. do something ..
            finally:
                ptt_bot.logout()

        參考 :ref:`使用者資料欄位 <user-field>`

        """

        return _api_get_user.get_user(self, user_id)

    def give_money(self, ptt_id: str, money: int, red_bag_title: Optional[str] = None,
                   red_bag_content: Optional[str] = None) -> None:

        """
        轉帳,詳見 `P 幣`_。

        .. _`P 幣`: https://pttpedia.fandom.com/zh/wiki/P%E5%B9%A3

        Args:
            ptt_id (str): PTT ID。
            money (int): 轉帳金額。
            red_bag_title (str): 紅包標題。
            red_bag_content (str): 紅包內容。

        Returns:
            None

        Raises:
            RequireLogin: 需要登入。
            UnregisteredUser: 未註冊使用者。
            NoSuchUser: 使用者不存在。
            NoMoney: 餘額不足。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.give_money(ptt_id='CodingMan', money=100)
                # or
                ptt_bot.give_money('CodingMan', 100, red_bag_title='紅包袋標題', red_bag_content='紅包袋內文')
                # .. do something ..
            finally:
                ptt_bot.logout()

        """

        _api_give_money.give_money(self, ptt_id, money, red_bag_title, red_bag_content)

    def mail(self, ptt_id: str, title: str, content: str, sign_file: [int | str] = 0,
             backup: bool = True) -> None:

        """
        寄信。

        Args:
            ptt_id (str): PTT ID。
            title (str): 信件標題。
            content (str): 信件內容。
            sign_file (str | int): 編號或隨機簽名檔 (x),預設為 0 (不選)。
            backup (bool): 如果是 True 寄信時將會備份信件,預設為 True。

        Returns:
            None

        Raises:
            RequireLogin: 需要登入。
            UnregisteredUser: 未註冊使用者。
            NoSuchUser: 使用者不存在。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.mail(ptt_id='CodingMan', title='信件標題', content='信件內容')
                # .. do something ..
            finally:
                ptt_bot.logout()
        """

        _api_mail.mail(self, ptt_id, title, content, sign_file, backup)

    def get_all_boards(self) -> List[str]:

        """
        取得全站看板清單。

        Returns:
            List[str],看板清單。

        Raises:
            RequireLogin: 需要登入。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                board_list = ptt_bot.get_all_boards()
                # .. do something ..
            finally:
                ptt_bot.logout()
        """

        return _api_get_board_list.get_board_list(self)

    def reply_post(self, reply_to: data_type.ReplyTo, board: str, content: str, sign_file: [str | int] = 0,
                   aid: Optional[str] = None, index: int = 0) -> None:

        """
        回覆文章。

        Args:
            reply_to (:ref:`reply-to`): 回覆類型。
            board (str): 看板名稱。
            content (str): 回覆內容。
            sign_file (str | int): 編號或隨機簽名檔 (x),預設為 **0** (不選)。
            aid: 文章編號。
            index: 文章編號。

        Returns:
            None

        Raises:
            RequireLogin: 需要登入。
            NoSuchBoard: 看板不存在。
            NoSuchPost: 文章不存在。
            NoPermission: 沒有回覆權限。
            CantResponse: 已結案並標記, 不得回應。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.reply_post(reply_to=PyPtt.ReplyTo.BOARD, board='Test', content='PyPtt 程式回覆測試', index=123)
                # .. do something ..
            finally:
                ptt_bot.logout()

        參考 :ref:`回覆類型 <reply-to>`、:ref:`取得最新文章編號 <api-get-newest-index>`
        """

        _api_reply_post.reply_post(self, reply_to, board, content, sign_file, aid, index)

    def set_board_title(self, board: str, new_title: str) -> None:
        """
        設定看板標題。

        Args:
            board (str): 看板名稱。
            new_title (str): 新標題。

        Returns:
            None

        Raises:
            RequireLogin: 需要登入。
            UnregisteredUser: 未註冊使用者。
            NoSuchBoard: 看板不存在。
            NeedModeratorPermission: 需要看板管理員權限。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.set_board_title(board='Test', new_title='現在時間 %s' % datetime.datetime.now())
                # .. do something ..
            finally:
                ptt_bot.logout()

        """

        _api_set_board_title.set_board_title(self, board, new_title)

    def mark_post(self, mark_type: int, board: str, aid: Optional[str] = None, index: int = 0, search_type: int = 0,
                  search_condition: Optional[str] = None) -> None:
        """
        標記文章。

        Args:
            mark_type (:ref:`mark-type`): 標記類型。
            board (str): 看板名稱。
            aid (str): 文章編號。
            index (int): 文章編號。
            search_type (:ref:`search-type`): 搜尋類型。
            search_condition (str): 搜尋條件。

        Returns:
            None

        Raises:
            RequireLogin: 需要登入。
            UnregisteredUser: 未註冊使用者。
            NoSuchBoard: 看板不存在。
            NoSuchPost: 文章不存在。
            NeedModeratorPermission: 需要看板管理員權限。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.mark_post(mark_type=PyPtt.MarkType.M, board='Test', index=123)
                # .. do something ..
            finally:
                ptt_bot.logout()
        """

        _api_mark_post.mark_post(self, mark_type, board, aid, index, search_type, search_condition)

    def get_favourite_boards(self) -> List[dict]:
        """
        取得我的最愛清單。

        Returns:
            List[dict],收藏看板清單,詳見 :ref:`favorite-board-field`。

        Raises:
            RequireLogin: 需要登入。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()

            try:
                # .. login ..
                favourite_boards = ptt_bot.get_favourite_boards()
                # .. do something ..
            finally:
                ptt_bot.logout()
        """

        return _api_get_favourite_board.get_favourite_board(self)

    def bucket(self, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:
        """
        水桶。

        Args:
            board (str): 看板名稱。
            bucket_days (int): 水桶天數。
            reason (str): 水桶原因。
            ptt_id (str): PTT ID。

        Returns:
            None

        Raises:
            RequireLogin: 需要登入。
            UnregisteredUser: 未註冊使用者。
            NoSuchBoard: 看板不存在。
            NoSuchUser: 使用者不存在。
            NeedModeratorPermission: 需要看板管理員權限。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.bucket(board='Test', bucket_days=7, reason='PyPtt 程式水桶測試', ptt_id='test')
                # .. do something ..
            finally:
                ptt_bot.logout()

        """

        _api_bucket.bucket(self, board, bucket_days, reason, ptt_id)

    def search_user(self, ptt_id: str, min_page: Optional[int] = None, max_page: Optional[int] = None) -> List[str]:
        """
        搜尋使用者。

        Args:
            ptt_id (str): PTT ID。
            min_page (int): 最小頁數。
            max_page (int): 最大頁數。

        Returns:
            List[str],搜尋結果。

        Raises:
            RequireLogin: 需要登入。
            UnregisteredUser: 未註冊使用者。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                search_result = ptt_bot.search_user(ptt_id='Coding')
                # .. do something ..
            finally:
                ptt_bot.logout()
        """

        return _api_search_user.search_user(self, ptt_id, min_page, max_page)

    def get_board_info(self, board: str, get_post_types: bool = False) -> Dict:
        """
        取得看板資訊。

        Args:
            board (str): 看板名稱。
            get_post_types (bool): 是否取得文章類型,例如:八卦板的「問卦」。

        Returns:
            Dict,看板資訊,詳見 :ref:`board-field`。

        Raises:
            RequireLogin: 需要登入。
            NoSuchBoard: 看板不存在。
            NoPermission: 沒有權限。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                board_info = ptt_bot.get_board_info(board='Test')
                # .. do something ..
            finally:
                ptt_bot.logout()
        """

        return _api_get_board_info.get_board_info(self, board, get_post_types, call_by_others=False)

    def get_mail(self, index: int, search_type: Optional[data_type.SearchType] = None,
                 search_condition: Optional[str] = None,
                 search_list: Optional[list] = None) -> Dict:
        """
        取得信件。

        Args:
            index (int): 信件編號。
            search_type (:ref:`search-type`): 搜尋類型。
            search_condition: 搜尋條件。
            search_list: 搜尋清單。

        Returns:
            Dict,信件資訊,詳見 :ref:`mail-field`。

        Raises:
            RequireLogin: 需要登入。
            NoSuchMail: 信件不存在。
            NoPermission: 沒有權限。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                mail = ptt_bot.get_mail(index=1)
                # .. do something ..
            finally:
                ptt_bot.logout()

        參考 :doc:`get_newest_index`
        """

        return _api_mail.get_mail(self, index, search_type, search_condition, search_list)

    def del_mail(self, index: int) -> None:
        """
        刪除信件。

        Args:
            index (int): 信件編號。

        Returns:
            None

        Raises:
            RequireLogin: 需要登入。
            UnregisteredUser: 未註冊使用者。
            MailboxFull: 信箱已滿。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.del_mail(index=1)
                # .. do something ..
            finally:
                ptt_bot.logout()

        參考 :doc:`get_newest_index`
        """

        _api_mail.del_mail(self, index)

    def change_pw(self, new_password: str) -> None:
        """
        更改密碼。
        備註:因批踢踢系統限制,最長密碼為 8 碼。

        Args:
            new_password (str): 新密碼。

        Returns:
            None

        Raises:
            RequireLogin: 需要登入。
            SetContactMailFirst: 需要先設定聯絡信箱。
            WrongPassword: 密碼錯誤。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.change_pw(new_password='123456')
                # .. do something ..
            finally:
                ptt_bot.logout()
        """

        _api_change_pw.change_pw(self, new_password)

    @functools.lru_cache(maxsize=64)
    def get_aid_from_url(self, url: str) -> Tuple[str, str]:
        """
        從網址取得看板名稱與文章編號。

        Args:
            url: 網址。

        Returns:
            Tuple[str, str],看板名稱與文章編號。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            url = 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html'
            board, aid = ptt_bot.get_aid_from_url(url)
        """

        return lib_util.get_aid_from_url(url)

    def get_bottom_post_list(self, board: str) -> List[str]:
        """
        取得看板置底文章清單。

        Args:
            board (str): 看板名稱。

        Returns:
            List[post],置底文章清單,詳見 :ref:`post-field`。

        Raises:
            RequireLogin: 需要登入。
            NoSuchBoard: 看板不存在。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()

            try:
                # .. login ..
                bottom_post_list = ptt_bot.get_bottom_post_list(board='Python')
                # .. do something ..
            finally:
                ptt_bot.logout()
        """

        return _api_get_bottom_post_list.get_bottom_post_list(self, board)

    def del_post(self, board: str, aid: Optional[str] = None, index: int = 0) -> None:
        """
        刪除文章。

        Args:
            board (str): 看板名稱。
            aid (str): 文章編號。
            index (int): 文章編號。

        Returns:
            None

        Raises:
            RequireLogin: 需要登入。
            UnregisteredUser: 未註冊使用者。
            NoSuchBoard: 看板不存在。
            NoSuchPost: 文章不存在。
            NoPermission: 沒有權限。

        範例::

            import PyPtt

            ptt_bot = PyPtt.API()
            try:
                # .. login ..
                ptt_bot.del_post(board='Python', aid='1TJH_XY0')
                # .. do something ..
            finally:
                ptt_bot.logout()
        """

        _api_del_post.del_post(self, board, aid, index)

    def fast_post_step0(self, board: str, title: str, content: str, post_type: int) -> None:
        _api_post.fast_post_step0(self, board, title, content, post_type)

    def fast_post_step1(self, sign_file):
        _api_post.fast_post_step1(self, sign_file)


if __name__ == '__main__':
    print('PyPtt v ' + __version__)
    print('Maintained by CodingMan')


================================================
FILE: PyPtt/__init__.py
================================================
__version__ = '1.1.2'

from .PTT import API
from .data_type import *
from .exceptions import *
from .log import LogLevel
from .service import Service

LOG_LEVEL = LogLevel

_main_version = '1.2'


================================================
FILE: PyPtt/_api_bucket.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import screens


def bucket(api, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    check_value.check_type(board, str, 'board')
    check_value.check_type(bucket_days, int, 'bucket_days')
    check_value.check_type(reason, str, 'reason')
    check_value.check_type(ptt_id, str, 'ptt_id')

    api.get_user(ptt_id)

    _api_util.check_board(api, board, check_moderator=True)

    _api_util.goto_board(api, board)

    cmd_list = []
    cmd_list.append('i')
    cmd_list.append(command.ctrl_p)
    cmd_list.append('w')
    cmd_list.append(command.enter)
    cmd_list.append('a')
    cmd_list.append(command.enter)
    cmd_list.append(ptt_id)
    cmd_list.append(command.enter)
    cmd = ''.join(cmd_list)

    cmd_list = []
    cmd_list.append(str(bucket_days))
    cmd_list.append(command.enter)
    cmd_list.append(reason)
    cmd_list.append(command.enter)
    cmd_list.append('y')
    cmd_list.append(command.enter)
    cmd_part2 = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('◆ 使用者之前已被禁言', exceptions_=exceptions.UserHasPreviouslyBeenBanned()),
        connect_core.TargetUnit('請以數字跟單位(預設為天)輸入期限', response=cmd_part2),
        connect_core.TargetUnit('其它鍵結束', response=command.enter),
        connect_core.TargetUnit('權限設定系統', response=command.enter),
        connect_core.TargetUnit('任意鍵', response=command.space),
        connect_core.TargetUnit(screens.Target.InBoard, break_detect=True),
    ]

    api.connect_core.send(
        cmd,
        target_list)


================================================
FILE: PyPtt/_api_call_status.py
================================================
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import log
from . import screens


def get_call_status(api) -> None:
    # log.py = DefaultLogger('api', api.config.log_level)

    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append('A')
    cmd_list.append(command.right)
    cmd_list.append(command.left)

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('[呼叫器]打開', log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit('[呼叫器]拔掉', log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit('[呼叫器]防水', log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit('[呼叫器]好友', log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit('[呼叫器]關閉', log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit('★', log_level=log.DEBUG, response=cmd),
    ]

    for i in range(2):
        index = api.connect_core.send(cmd, target_list)
        if index < 0:
            if i == 0:
                continue
            raise exceptions.UnknownError('UnknownError')

    if index == 0:
        return data_type.call_status.ON
    if index == 1:
        return data_type.call_status.UNPLUG
    if index == 2:
        return data_type.call_status.WATERPROOF
    if index == 3:
        return data_type.call_status.FRIEND
    if index == 4:
        return data_type.call_status.OFF

    ori_screen = api.connect_core.get_screen_queue()[-1]
    raise exceptions.UnknownError(ori_screen)


def set_call_status(api, call_status) -> None:
    # 打開 -> 拔掉 -> 防水 -> 好友 -> 關閉

    current_call_status = api._get_call_status()

    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append(command.ctrl_u)
    cmd_list.append('p')

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit(screens.Target.InUserList, break_detect=True)]

    while current_call_status != call_status:
        api.connect_core.send(
            cmd,
            target_list,
            screen_timeout=api.config.screen_long_timeout)

        current_call_status = api._get_call_status()


================================================
FILE: PyPtt/_api_change_pw.py
================================================
from . import command, _api_util
from . import connect_core
from . import exceptions
from . import i18n
from . import log


def change_pw(api, new_password: str) -> None:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    log.logger.info(i18n.change_pw)

    new_password = new_password[:8]

    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append('U')
    cmd_list.append(command.enter)
    cmd_list.append('I')
    cmd_list.append(command.enter)
    cmd_list.append('2')
    cmd_list.append(command.enter)
    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('設定聯絡信箱後才能修改密碼', exceptions_=exceptions.SetContactMailFirst()),
        connect_core.TargetUnit('您輸入的密碼不正確', exceptions_=exceptions.WrongPassword()),
        connect_core.TargetUnit('請您確定(Y/N)?', response='Y' + command.enter),
        connect_core.TargetUnit('檢查新密碼', response=new_password + command.enter, max_match=1),
        connect_core.TargetUnit('設定新密碼', response=new_password + command.enter, max_match=1),
        connect_core.TargetUnit('輸入原密碼', response=api._ptt_pw + command.enter, max_match=1),
        connect_core.TargetUnit('設定個人資料與密碼', break_detect=True)
    ]

    index = api.connect_core.send(
        cmd,
        target_list,
        screen_timeout=api.config.screen_long_timeout)
    if index < 0:
        ori_screen = api.connect_core.get_screen_queue()[-1]
        raise exceptions.UnknownError(ori_screen)

    api._ptt_pw = new_password

    log.logger.info(i18n.change_pw, '...', i18n.success)


================================================
FILE: PyPtt/_api_comment.py
================================================
import collections
import time

from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens

comment_option = [
    None,
    data_type.CommentType.PUSH,
    data_type.CommentType.BOO,
    data_type.CommentType.ARROW,
]


def _comment(api,
             board: str,
             push_type: data_type.CommentType,
             push_content: str,
             post_aid: str,
             post_index: int) -> None:
    _api_util.goto_board(api, board)

    cmd_list = []

    if post_aid is not None:
        cmd_list.append(lib_util.check_aid(post_aid))
    elif post_index != 0:
        cmd_list.append(str(post_index))
    else:
        raise ValueError('post_aid and post_index cannot be None at the same time')

    cmd_list.append(command.enter)
    cmd_list.append(command.comment)

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('您覺得這篇', log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit(f'→ {api.ptt_id}: ', log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit('加註方式', log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit('禁止快速連續推文', log_level=log.INFO, break_detect=True,
                                exceptions_=exceptions.NoFastComment()),
        connect_core.TargetUnit('禁止短時間內大量推文', log_level=log.INFO, break_detect=True,
                                exceptions_=exceptions.NoFastComment()),
        connect_core.TargetUnit('使用者不可發言', log_level=log.INFO, break_detect=True,
                                exceptions_=exceptions.NoPermission(i18n.no_permission)),
        connect_core.TargetUnit('◆ 抱歉, 禁止推薦', log_level=log.INFO, break_detect=True,
                                exceptions_=exceptions.CantComment()),
    ]

    index = api.connect_core.send(
        cmd,
        target_list)

    if index == -1:
        raise exceptions.UnknownError('unknown error in comment')

    log.logger.debug(i18n.has_comment_permission)

    cmd_list = []

    if index == 0 or index == 1:
        push_option_line = api.connect_core.get_screen_queue()[-1]
        push_option_line = push_option_line.split('\n')[-1]

        log.logger.debug('comment option line', push_option_line)

        available_push_type = collections.defaultdict(lambda: False)
        first_available_push_type = None

        if '值得推薦' in push_option_line:
            available_push_type[data_type.CommentType.PUSH] = True

            if first_available_push_type is None:
                first_available_push_type = data_type.CommentType.PUSH

        if '只加→註解' in push_option_line:
            available_push_type[data_type.CommentType.ARROW] = True

            if first_available_push_type is None:
                first_available_push_type = data_type.CommentType.ARROW

        if '給它噓聲' in push_option_line:
            available_push_type[data_type.CommentType.BOO] = True

            if first_available_push_type is None:
                first_available_push_type = data_type.CommentType.BOO

        log.logger.debug('available_push_type', available_push_type)

        if available_push_type[push_type] is False:
            if first_available_push_type:
                push_type = first_available_push_type

        if True in available_push_type.values():
            cmd_list.append(str(comment_option.index(push_type)))

    cmd_list.append(push_content)
    cmd_list.append(command.enter)
    cmd_list.append('y')
    cmd_list.append(command.enter)

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
    ]

    api.connect_core.send(
        cmd,
        target_list)


def comment(api, board: str, push_type: data_type.CommentType, push_content: str, post_aid: str,
            post_index: int) -> None:
    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    check_value.check_type(board, str, 'board')

    if not isinstance(push_type, data_type.CommentType):
        raise TypeError(f'CommentType must be data_type.CommentType')

    check_value.check_type(push_content, str, 'push_content')
    if post_aid is not None:
        check_value.check_type(post_aid, str, 'aid')
    check_value.check_type(post_index, int, 'index')

    if len(board) == 0:
        raise ValueError(f'wrong parameter board: {board}')

    if post_index != 0 and isinstance(post_aid, str):
        raise ValueError('wrong parameter index and aid can\'t both input')

    if post_index == 0 and post_aid is None:
        raise ValueError('wrong parameter index or aid must input')

    if post_index != 0:
        newest_index = api.get_newest_index(
            data_type.NewIndex.BOARD,
            board=board)
        check_value.check_index('index', post_index, newest_index)

    _api_util.check_board(api, board)

    board_info = api._board_info_list[board.lower()]

    if board_info[data_type.BoardField.is_comment_record_ip]:
        log.logger.debug(i18n.record_ip)
        if board_info[data_type.BoardField.is_comment_aligned]:
            log.logger.debug(i18n.push_aligned)
            max_push_length = 32
        else:
            log.logger.debug(i18n.not_push_aligned)
            max_push_length = 43 - len(api.ptt_id)
    else:
        log.logger.debug(i18n.not_record_ip)
        if board_info[data_type.BoardField.is_comment_aligned]:
            log.logger.debug(i18n.push_aligned)
            max_push_length = 46
        else:
            log.logger.debug(i18n.not_push_aligned)
            max_push_length = 58 - len(api.ptt_id)

    push_content = push_content.strip()

    push_list = []
    while push_content:
        index = 0
        jump = 0

        while len(push_content[:index].encode('big5uao', 'replace')) < max_push_length:

            if index == len(push_content):
                break
            if push_content[index] == '\n':
                jump = 1
                break

            index += 1

        push_list.append(push_content[:index])
        push_content = push_content[index + jump:]

    push_list = filter(None, push_list)

    for comment in push_list:

        log.logger.info(i18n.comment)

        for _ in range(2):
            try:
                _comment(api, board, push_type, comment, post_aid=post_aid, post_index=post_index)
                break
            except exceptions.NoFastComment:
                # screens.show(api.config, api.connect_core.getScreenQueue())
                log.logger.info(i18n.wait_for_no_fast_comment)
                time.sleep(5.2)

        log.logger.info(i18n.comment, '...', i18n.success)


================================================
FILE: PyPtt/_api_del_post.py
================================================
from __future__ import annotations

from typing import Optional

from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens


def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0) -> None:
    _api_util.one_thread(api)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    check_value.check_type(board, str, 'board')
    if post_aid is not None:
        check_value.check_type(post_aid, str, 'PostAID')
    check_value.check_type(post_index, int, 'PostIndex')

    if len(board) == 0:
        raise ValueError(f'board error parameter: {board}')

    if post_index != 0 and isinstance(post_aid, str):
        raise ValueError('wrong parameter index and aid can\'t both input')

    if post_index == 0 and post_aid is None:
        raise ValueError('wrong parameter index or aid must input')

    if post_index != 0:
        newest_index = api.get_newest_index(
            data_type.NewIndex.BOARD,
            board=board)
        check_value.check_index(
            'PostIndex',
            post_index,
            newest_index)

    log.logger.info(i18n.delete_post)

    board_info = _api_util.check_board(api, board)

    check_author = True
    for moderator in board_info[data_type.BoardField.moderators]:
        if api.ptt_id.lower() == moderator.lower():
            check_author = False
            break

    log.logger.info(i18n.delete_post)

    post_info = api.get_post(board, aid=post_aid, index=post_index, query=True)
    if post_info[data_type.PostField.post_status] != data_type.PostStatus.EXISTS:
        # delete success
        log.logger.info(i18n.success)
        return

    if check_author:
        if api.ptt_id.lower() != post_info[data_type.PostField.author].lower():
            log.logger.info(i18n.delete_post, '...', i18n.fail)
            raise exceptions.NoPermission(i18n.no_permission)

    _api_util.goto_board(api, board)

    cmd_list = []

    if post_aid is not None:
        cmd_list.append(lib_util.check_aid(post_aid))
    elif post_index != 0:
        cmd_list.append(str(post_index))
    else:
        raise ValueError('post_aid and post_index cannot be None at the same time')

    cmd_list.append(command.enter)
    cmd_list.append('d')

    cmd = ''.join(cmd_list)

    api.confirm = False

    def confirm_delete_handler(screen):
        api.confirm = True

    target_list = [
        connect_core.TargetUnit('請按任意鍵繼續', response=' '),
        connect_core.TargetUnit('請確定刪除(Y/N)?[N]', response='y' + command.enter, handler=confirm_delete_handler,
                                max_match=1),
        connect_core.TargetUnit(screens.Target.InBoard, break_detect=True),
    ]

    index = api.connect_core.send(
        cmd,
        target_list)

    if index == 1:
        if not api.confirm:
            log.logger.info(i18n.delete_post, '...', i18n.fail)
            raise exceptions.NoPermission(i18n.no_permission)

    log.logger.info(i18n.delete_post, '...', i18n.success)

================================================
FILE: PyPtt/_api_get_board_info.py
================================================
import re
from typing import Dict

from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens
from .data_type import BoardField


def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -> Dict:
    logger = log.init(log.DEBUG if call_by_others else log.INFO)

    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    check_value.check_type(board, str, 'board')

    logger.info(
        i18n.replace(i18n.get_board_info, board))

    _api_util.goto_board(api, board, refresh=True)

    ori_screen = api.connect_core.get_screen_queue()[-1]
    # print(ori_screen)

    nuser = None
    for line in ori_screen.split('\n'):
        if '編號' not in line:
            continue
        if '日 期' not in line:
            continue
        if '人氣' not in line:
            continue

        nuser = line
        break

    if nuser is None:
        raise exceptions.NoSuchBoard(api.config, board)

    # print('------------------------')
    # print('nuser', nuser)
    # print('------------------------')
    if '[靜]' in nuser:
        online_user = 0
    else:
        if '編號' not in nuser or '人氣' not in nuser:
            raise exceptions.NoSuchBoard(api.config, board)
        pattern = re.compile('[\d]+')
        r = pattern.search(nuser)
        if r is None:
            raise exceptions.NoSuchBoard(api.config, board)
        # 減一是把自己本身拿掉
        online_user = int(r.group(0)) - 1

    logger.debug('人氣', online_user)

    target_list = [
        connect_core.TargetUnit('任意鍵繼續', log_level=log.DEBUG if call_by_others else log.INFO,
                                break_detect=True),
    ]

    api.connect_core.send(
        'i',
        target_list)

    ori_screen = api.connect_core.get_screen_queue()[-1]
    # print(ori_screen)

    p = re.compile('《(.+)》看板設定')
    r = p.search(ori_screen)
    if r is not None:
        boardname = r.group(0)[1:-5].strip()

    logger.debug('看板名稱', boardname, board)

    if boardname.lower() != board.lower():
        raise exceptions.NoSuchBoard(api.config, board)

    p = re.compile('中文敘述: (.+)')
    r = p.search(ori_screen)
    if r is not None:
        chinese_des = r.group(0)[5:].strip()
    logger.debug('中文敘述', chinese_des)

    p = re.compile('板主名單: (.+)')
    r = p.search(ori_screen)
    if r is not None:
        moderator_line = r.group(0)[5:].strip()
        if '(無)' in moderator_line:
            moderators = []
        else:
            moderators = moderator_line.split('/')
            for moderator in moderators.copy():
                if moderator == '徵求中':
                    moderators.remove(moderator)
    logger.debug('板主名單', moderators)

    open_status = ('公開狀態(是否隱形): 公開' in ori_screen)
    logger.debug('公開狀態', open_status)

    into_top_ten_when_hide = ('隱板時 可以 進入十大排行榜' in ori_screen)
    logger.debug('隱板時可以進入十大排行榜', into_top_ten_when_hide)

    non_board_members_post = ('開放 非看板會員發文' in ori_screen)
    logger.debug('非看板會員發文', non_board_members_post)

    reply_post = ('開放 回應文章' in ori_screen)
    logger.debug('回應文章', reply_post)

    self_del_post = ('開放 自刪文章' in ori_screen)
    logger.debug('自刪文章', self_del_post)

    push_post = ('開放 推薦文章' in ori_screen)
    logger.debug('推薦文章', push_post)

    boo_post = ('開放 噓文' in ori_screen)
    logger.debug('噓文', boo_post)

    # 限制 快速連推文章, 最低間隔時間: 5 秒
    # 開放 快速連推文章

    fast_push = ('開放 快速連推文章' in ori_screen)
    logger.debug('快速連推文章', fast_push)

    if not fast_push:
        p = re.compile('最低間隔時間: [\d]+')
        r = p.search(ori_screen)
        if r is not None:
            min_interval = r.group(0)[7:].strip()
            min_interval = int(min_interval)
        else:
            min_interval = 0
        logger.debug('最低間隔時間', min_interval)
    else:
        min_interval = 0

    # 推文時 自動 記錄來源 IP
    # 推文時 不會 記錄來源 IP
    push_record_ip = ('推文時 自動 記錄來源 IP' in ori_screen)
    logger.debug('記錄來源 IP', push_record_ip)

    # 推文時 對齊 開頭
    # 推文時 不用對齊 開頭
    push_aligned = ('推文時 對齊 開頭' in ori_screen)
    logger.debug('對齊開頭', push_aligned)

    # 板主 可 刪除部份違規文字
    moderator_can_del_illegal_content = ('板主 可 刪除部份違規文字' in ori_screen)
    logger.debug('板主可刪除部份違規文字', moderator_can_del_illegal_content)

    # 轉錄文章 會 自動記錄,且 需要 發文權限
    tran_post_auto_recorded_and_require_post_permissions = ('轉錄文章 會 自動記錄,且 需要 發文權限' in ori_screen)
    logger.debug('轉錄文章 會 自動記錄,且 需要 發文權限', tran_post_auto_recorded_and_require_post_permissions)

    cool_mode = ('未 設為冷靜模式' not in ori_screen)
    logger.debug('冷靜模式', cool_mode)

    require18 = ('禁止 未滿十八歲進入' in ori_screen)
    logger.debug('禁止未滿十八歲進入', require18)

    p = re.compile('登入次數 [\d]+ 次以上')
    r = p.search(ori_screen)
    if r is not None:
        require_login_time = r.group(0).split(' ')[1]
        require_login_time = int(require_login_time)
    else:
        require_login_time = 0
    logger.debug('發文限制登入次數', require_login_time)

    p = re.compile('退文篇數 [\d]+ 篇以下')
    r = p.search(ori_screen)
    if r is not None:
        require_illegal_post = r.group(0).split(' ')[1]
        require_illegal_post = int(require_illegal_post)
    else:
        require_illegal_post = 0
    logger.debug('發文限制退文篇數', require_illegal_post)

    kind_list = []
    if get_post_kind:

        _api_util.goto_board(api, board)

        # Go certain board, then post to get post type info
        cmd_list = []
        cmd_list.append(command.ctrl_p)
        cmd = ''.join(cmd_list)

        target_list = [
            connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
            connect_core.TargetUnit('或不選)', break_detect=True)
        ]

        index = api.connect_core.send(
            cmd,
            target_list)

        if index == 0:
            raise exceptions.NoPermission(i18n.no_permission)
            # no post permission

        ori_screen = api.connect_core.get_screen_queue()[-1]
        screen_lines = ori_screen.split('\n')

        for i in screen_lines:
            if '種類:' in i:
                type_pattern = re.compile('\d\.([^\ ]*)')
                # 0 is not present any type that the key hold None object
                kind_list = type_pattern.findall(i)
                break

        # Clear post status
        cmd_list = []
        cmd_list.append(command.ctrl_c)
        cmd_list.append(command.ctrl_c)
        cmd = ''.join(cmd_list)

        target_list = [
            connect_core.TargetUnit(screens.Target.InBoard, break_detect=True)
        ]
        api.connect_core.send(
            cmd,
            target_list)

    logger.info(
        i18n.replace(i18n.get_board_info, board),
        '...', i18n.success
    )

    return {
        BoardField.board: boardname,
        BoardField.online_user: online_user,
        BoardField.mandarin_des: chinese_des,
        BoardField.moderators: moderators,
        BoardField.open_status: open_status,
        BoardField.into_top_ten_when_hide: into_top_ten_when_hide,
        BoardField.can_non_board_members_post: non_board_members_post,
        BoardField.can_reply_post: reply_post,
        BoardField.self_del_post: self_del_post,
        BoardField.can_comment_post: push_post,
        BoardField.can_boo_post: boo_post,
        BoardField.can_fast_push: fast_push,
        BoardField.min_interval_between_comments: min_interval,
        BoardField.is_comment_record_ip: push_record_ip,
        BoardField.is_comment_aligned: push_aligned,
        BoardField.can_moderators_del_illegal_content: moderator_can_del_illegal_content,
        BoardField.does_tran_post_auto_recorded_and_require_post_permissions: tran_post_auto_recorded_and_require_post_permissions,
        BoardField.is_cool_mode: cool_mode,
        BoardField.is_require18: require18,
        BoardField.require_login_time: require_login_time,
        BoardField.require_illegal_post: require_illegal_post,
        BoardField.post_kind_list: kind_list
    }


================================================
FILE: PyPtt/_api_get_board_list.py
================================================
import progressbar

from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens


def get_board_list(api) -> list:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    cmd_list = [
        command.go_main_menu,
        'F',
        command.enter,
        'y',
        '$']
    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit(screens.Target.InBoardList, break_detect=True)
    ]

    api.connect_core.send(
        cmd,
        target_list,
        screen_timeout=api.config.screen_long_timeout)
    ori_screen = api.connect_core.get_screen_queue()[-1]

    max_no = 0
    for line in ori_screen.split('\n'):
        if '◎' not in line and '●' not in line:
            continue

        if line.startswith(api.cursor):
            line = line[len(api.cursor):]

        # print(f'->{line}<')
        if '◎' in line:
            front_part = line[:line.find('◎')]
        else:
            front_part = line[:line.find('●')]
        front_part_list = [x for x in front_part.split(' ')]
        front_part_list = list(filter(None, front_part_list))
        # print(f'FrontPartList =>{FrontPartList}<=')
        max_no = int(front_part_list[0].rstrip(')'))

    if api.config.log_level == log.INFO:
        pb = progressbar.ProgressBar(
            max_value=max_no,
            redirect_stdout=True)

    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append('F')
    cmd_list.append(command.enter)
    cmd_list.append('y')
    cmd_list.append('0')
    cmd = ''.join(cmd_list)

    board_list = []
    while True:

        api.connect_core.send(
            cmd,
            target_list,
            screen_timeout=api.config.screen_long_timeout)

        ori_screen = api.connect_core.get_screen_queue()[-1]
        # print(OriScreen)
        for line in ori_screen.split('\n'):
            if '◎' not in line and '●' not in line:
                continue

            if line.startswith(api.cursor):
                line = line[len(api.cursor):]

            if '◎' in line:
                front_part = line[:line.find('◎')]
            else:
                front_part = line[:line.find('●')]
            front_part_list = [x for x in front_part.split(' ')]
            front_part_list = list(filter(None, front_part_list))

            number = front_part_list[0]
            if ')' in number:
                number = number[:number.rfind(')')]
            no = int(number)

            board_name = front_part_list[1]
            if board_name.startswith('ˇ'):
                board_name = board_name[1:]
                if len(board_name) == 0:
                    board_name = front_part_list[2]

            board_list.append(board_name)

            if api.config.log_level == log.INFO:
                pb.update(no)

        if no >= max_no:
            break
        cmd = command.ctrl_f

    if api.config.log_level == log.INFO:
        pb.finish()

    return board_list


================================================
FILE: PyPtt/_api_get_bottom_post_list.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import log
from . import screens


def get_bottom_post_list(api, board):
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    check_value.check_type(board, str, 'board')

    log.logger.info(i18n.catch_bottom_post)

    _api_util.check_board(api, board)

    _api_util.goto_board(api, board, end=True)

    last_screen = api.connect_core.get_screen_queue()[-1]

    bottom_screen = [line for line in last_screen.split('\n') if '★' in line[:8]]
    bottom_length = len(bottom_screen)

    if bottom_length == 0:
        log.logger.info(i18n.catch_bottom_post_success)
        return list()

    cmd_list = []
    cmd_list.append(command.query_post)
    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit(
            screens.Target.PTT1_QueryPost if api.config.host == data_type.HOST.PTT1 else screens.Target.PTT2_QueryPost,
            log_level=log.DEBUG, break_detect=True, refresh=False),
        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)),
    ]

    aid_list = []

    result = []
    for _ in range(bottom_length):
        api.connect_core.send(cmd, target_list)
        last_screen = api.connect_core.get_screen_queue()[-1]

        lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index = \
            _api_util.parse_query_post(
                api,
                last_screen)

        aid_list.append(post_aid)

        cmd_list = []
        cmd_list.append(command.enter)
        cmd_list.append(command.up)
        cmd_list.append(command.query_post)
        cmd = ''.join(cmd_list)

    aid_list.reverse()

    for post_aid in aid_list:
        current_post = api.get_post(board=board, aid=post_aid, query=True)
        result.append(current_post)

    log.logger.info(i18n.catch_bottom_post, '...', i18n.success)

    return list(reversed(result))


================================================
FILE: PyPtt/_api_get_favourite_board.py
================================================
from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from .data_type import FavouriteBoardField


def get_favourite_board(api) -> list:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    cmd_list = [command.go_main_menu, 'F', command.enter, '0']
    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('選擇看板', break_detect=True)
    ]

    log.logger.info(i18n.get_favourite_board_list)

    board_list = []
    favourite_board_list = []
    while True:

        api.connect_core.send(
            cmd,
            target_list)

        ori_screen = api.connect_core.get_screen_queue()[-1]
        # print(OriScreen)
        screen_buf = ori_screen
        screen_buf = [x for x in screen_buf.split('\n')][3:-1]

        # adjust for cursor
        screen_buf[0] = '  ' + screen_buf[0][1:]
        screen_buf = [x for x in screen_buf]

        min_len = 47

        for i, line in enumerate(screen_buf):
            if len(screen_buf[i]) == 0:
                continue
            if len(screen_buf[i]) <= min_len:
                # print(f'[{ScreenBuf[i]}]')
                screen_buf[i] = screen_buf[i] + (' ' * ((min_len + 1) - len(screen_buf[i])))
        screen_buf = [x[10:min_len - len(x)].strip() for x in screen_buf]
        screen_buf = list(filter(None, screen_buf))

        for i, line in enumerate(screen_buf):
            if '------------' in line:
                continue

            temp = line.strip().split(' ')
            no_space_temp = list(filter(None, temp))

            board = no_space_temp[0]
            if board.startswith('ˇ'):
                board = board[1:]

            board_type = no_space_temp[1]

            title_start_index = temp.index(board_type) + 1
            board_title = ' '.join(temp[title_start_index:])
            # remove ◎
            board_title = board_title[1:]

            if board in board_list:
                log.logger.info(i18n.success)
                return favourite_board_list
            board_list.append(board)

            favourite_board_list.append({
                FavouriteBoardField.board: board,
                FavouriteBoardField.type: board_type,
                FavouriteBoardField.title: board_title})

        if len(screen_buf) < 20:
            break

        cmd = command.ctrl_f

    log.logger.info(i18n.get_favourite_board_list, '...', i18n.success)
    return favourite_board_list


================================================
FILE: PyPtt/_api_get_newest_index.py
================================================
from __future__ import annotations

import re
from typing import Optional

from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type, lib_util
from . import exceptions
from . import i18n
from . import log
from . import screens


def _get_newest_index(api) -> int:
    last_screen = api.connect_core.get_screen_queue()[-1]

    last_screen_list = last_screen.split('\n')
    last_screen_list = last_screen_list[3:]
    last_screen_list = '\n'.join([x[:9] for x in last_screen_list])
    # print(last_screen_list)
    all_index = re.findall(r'\d+', last_screen_list)

    if len(all_index) == 0:
        return 0

    all_index = list(map(int, all_index))
    all_index.sort(reverse=True)
    # print(all_index)

    max_check_range = 6
    newest_index = 0
    for index_temp in all_index:
        need_continue = True
        if index_temp > max_check_range:
            check_range = max_check_range
        else:
            check_range = index_temp
        for i in range(1, check_range):
            if str(index_temp - i) not in last_screen:
                need_continue = False
                break
        if need_continue:
            log.logger.debug(i18n.find_newest_index, index_temp)
            newest_index = index_temp
            break

    if newest_index == 0:
        raise exceptions.UnknownError('UnknownError')

    return newest_index


def get_newest_index(api, index_type: data_type.NewIndex, board: Optional[str] = None,
                     search_type: data_type.SearchType = None, search_condition: Optional[str] = None,
                     search_list: Optional[list] = None) -> int:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if search_list is None:
        search_list = []
    else:
        check_value.check_type(search_list, list, 'search_list')

    if (search_type, search_condition) != (None, None):
        search_list.insert(0, (search_type, search_condition))

    for search_type, search_condition in search_list:
        check_value.check_type(search_type, data_type.SearchType, 'search_type')
        check_value.check_type(search_condition, str, 'search_condition')

    check_value.check_type(index_type, data_type.NewIndex, 'index_type')

    data_key = f'{index_type}_{board}_{search_list}'
    if data_key in api._newest_index_data:
        return api._newest_index_data[data_key]

    if index_type == data_type.NewIndex.BOARD:

        check_value.check_type(board, str, 'board')

        _api_util.check_board(api, board)
        _api_util.goto_board(api, board)

        cmd_list = []
        cmd_list.append('1')
        cmd_list.append(command.enter)
        cmd_list.append('$')

        cmd = ''.join(cmd_list)

        target_list = [
            connect_core.TargetUnit('沒有文章...', log_level=log.DEBUG, break_detect=True),
            connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
            connect_core.TargetUnit(screens.Target.MainMenu_Exiting,
                                    exceptions_=exceptions.NoSuchBoard(api.config, board)),
        ]

        index = api.connect_core.send(cmd, target_list)
        if index < 0:
            raise exceptions.NoSuchBoard(api.config, board)

        if index == 0:
            return 0

        normal_newest_index = _get_newest_index(api)

        if search_list is not None and len(search_list) > 0:
            target_list.insert(2,
                               connect_core.TargetUnit(
                                   screens.Target.InBoardWithCursor,
                                   log_level=log.DEBUG,
                                   break_detect=True))

            cmd_list = _api_util.get_search_condition_cmd(index_type, search_list)

            cmd_list.append('1')
            cmd_list.append(command.enter)
            cmd_list.append('$')
            cmd = ''.join(cmd_list)

            index = api.connect_core.send(cmd, target_list)
            if index < 0:
                raise exceptions.NoSuchBoard(api.config, board)

            if index == 0:
                return 0

            newest_index = _get_newest_index(api)

            if normal_newest_index == newest_index:
                raise exceptions.NoSearchResult()
        else:
            newest_index = normal_newest_index

    elif index_type == data_type.NewIndex.MAIL:

        if not api.is_registered_user:
            raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

        if board is not None:
            raise ValueError('board should not input at NewIndex.MAIL.')

        cmd_list = []
        cmd_list.append(command.go_main_menu)
        cmd_list.append(command.ctrl_z)
        cmd_list.append('m')

        cmd_list.append('1')
        cmd_list.append(command.enter)
        cmd_list.append('$')

        cmd = ''.join(cmd_list)

        target_list = [
            connect_core.TargetUnit(screens.Target.InMailBox, break_detect=True),
            connect_core.TargetUnit(screens.Target.CursorToGoodbye, response=cmd),
        ]

        def get_index(api):
            current_capacity, _ = _api_util.get_mailbox_capacity(api)
            last_screen = api.connect_core.get_screen_queue()[-1]
            cursor_line = [x for x in last_screen.split('\n') if x.strip().startswith(api.cursor)][0]

            list_index = int(re.compile('(\d+)').search(cursor_line).group(0))

            if search_type == 0 and search_list is None:
                if list_index > current_capacity:
                    newest_index = list_index
                else:
                    newest_index = current_capacity
            else:
                newest_index = list_index

            return newest_index

        newest_index = 0

        index = api.connect_core.send(
            cmd,
            target_list)

        if index == 0:
            normal_newest_index = get_index(api)

            if search_list is not None and len(search_list) > 0:
                target_list.insert(
                    2,
                    connect_core.TargetUnit(
                        screens.Target.InMailBoxWithCursor,
                        log_level=log.DEBUG,
                        break_detect=True)
                )

                cmd_list = []
                cmd_list.append(command.go_main_menu)
                cmd_list.append(command.ctrl_z)
                cmd_list.append('m')

                cmd_list.extend(
                    _api_util.get_search_condition_cmd(index_type, search_list)
                )

                cmd_list.append('1')
                cmd_list.append(command.enter)
                cmd_list.append('$')

                cmd = ''.join(cmd_list)

                index = api.connect_core.send(
                    cmd,
                    target_list)

                if index in [0, 2]:
                    newest_index = get_index(api)
                    if normal_newest_index == newest_index:
                        raise exceptions.NoSearchResult()

            else:
                newest_index = normal_newest_index

    api._newest_index_data[data_key] = newest_index
    return newest_index


================================================
FILE: PyPtt/_api_get_post.py
================================================
from __future__ import annotations

import json
import re
import time
from typing import Dict, Optional

from AutoStrEnum import AutoJsonEncoder

from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
from .data_type import PostField, CommentField


def get_post(api, board: str, aid: Optional[str] = None, index: Optional[int] = None,
             search_list: Optional[list] = None,
             search_type: Optional[data_type.SearchType] = None,
             search_condition: Optional[str] = None, query: bool = False) -> Dict:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    check_value.check_type(board, str, 'board')
    if aid is not None:
        check_value.check_type(aid, str, 'aid')
    if index is not None:
        check_value.check_type(index, int, 'index')

    if search_list is None:
        search_list = []
    else:
        check_value.check_type(search_list, list, 'search_list')

    if (search_type, search_condition) != (None, None):
        search_list.insert(0, (search_type, search_condition))

    for search_type, search_condition in search_list:
        check_value.check_type(search_type, data_type.SearchType, 'search_type')
        check_value.check_type(search_condition, str, 'search_condition')

    if len(board) == 0:
        raise ValueError(f'board error parameter: {board}')

    if index is not None and isinstance(aid, str):
        raise ValueError('wrong parameter index and aid can\'t both input')

    if index is None and aid is None:
        raise ValueError('wrong parameter index or aid must input')

    search_cmd = None
    if search_list is not None and len(search_list) > 0:
        current_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board, search_list=search_list)
        search_cmd = _api_util.get_search_condition_cmd(data_type.NewIndex.BOARD, search_list)
    else:
        current_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board)

    if index is not None:
        check_value.check_index('index', index, current_index)

    max_retry = 2
    post = {}
    for i in range(max_retry):
        try:
            post = _get_post(api, board, aid, index, query, search_cmd)
            if not post:
                pass
            elif not post[PostField.pass_format_check]:
                pass
            else:
                break
        except exceptions.UnknownError:
            if i == max_retry - 1:
                raise
        except exceptions.NoSuchBoard:
            if i == max_retry - 1:
                raise

        log.logger.debug('Wait for retry repost')
        time.sleep(0.1)

    post = json.dumps(post, cls=AutoJsonEncoder)
    return json.loads(post)


def _get_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0, query: bool = False,
              search_cmd_list: Optional[list[str]] = None) -> Dict:
    _api_util.check_board(api, board)
    _api_util.goto_board(api, board)

    cmd_list = []

    if post_aid is not None:
        cmd_list.append(lib_util.check_aid(post_aid))
    elif post_index != 0:

        if search_cmd_list is not None:
            cmd_list.extend(search_cmd_list)

        cmd_list.append(str(max(1, post_index - 100)))
        cmd_list.append(command.enter)
        cmd_list.append(str(post_index))
    else:
        raise ValueError('post_aid and post_index cannot be None at the same time')

    cmd_list.append(command.enter)
    cmd_list.append(command.query_post)

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit(
            screens.Target.PTT1_QueryPost if api.config.host == data_type.HOST.PTT1 else screens.Target.PTT2_QueryPost,
            log_level=log.DEBUG, break_detect=True, refresh=False),
        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)),
    ]

    index = api.connect_core.send(cmd, target_list)
    ori_screen = api.connect_core.get_screen_queue()[-1]

    post = {
        PostField.board: None,
        PostField.aid: None,
        PostField.index: None,
        PostField.author: None,
        PostField.date: None,
        PostField.title: None,
        PostField.content: None,
        PostField.money: None,
        PostField.url: None,
        PostField.ip: None,
        PostField.comments: [],
        PostField.post_status: data_type.PostStatus.EXISTS,
        PostField.list_date: None,
        PostField.has_control_code: False,
        PostField.pass_format_check: False,
        PostField.location: None,
        PostField.push_number: None,
        PostField.is_lock: False,
        PostField.full_content: None,
        PostField.is_unconfirmed: False}

    post_author = None
    post_title = None
    if index < 0 or index == 1:
        # 文章被刪除
        log.logger.debug(i18n.post_deleted)
        log.logger.debug('OriScreen', ori_screen)

        cursor_line = [line for line in ori_screen.split(
            '\n') if line.startswith(api.cursor)]

        if len(cursor_line) != 1:
            raise exceptions.UnknownError(ori_screen)

        cursor_line = cursor_line[0]
        log.logger.debug('CursorLine', cursor_line)

        pattern = re.compile('[\d]+\/[\d]+')
        pattern_result = pattern.search(cursor_line)
        if pattern_result is None:
            list_date = None
        else:
            list_date = pattern_result.group(0)
            list_date = list_date[-5:]

        pattern = re.compile('\[[\w]+\]')
        pattern_result = pattern.search(cursor_line)
        if pattern_result is not None:
            post_del_status = data_type.PostStatus.DELETED_BY_AUTHOR
        else:
            pattern = re.compile('<[\w]+>')
            pattern_result = pattern.search(cursor_line)
            post_del_status = data_type.PostStatus.DELETED_BY_MODERATOR

        # > 79843     9/11 -             □ (本文已被吃掉)<
        # > 76060     8/28 -             □ (本文已被刪除) [weida7332]
        # print(f'O=>{CursorLine}<')
        if pattern_result is not None:
            post_author = pattern_result.group(0)[1:-1]
        else:
            post_author = None
            post_del_status = data_type.PostStatus.DELETED_BY_UNKNOWN

        log.logger.debug('ListDate', list_date)
        log.logger.debug('PostAuthor', post_author)
        log.logger.debug('post_del_status', post_del_status)

        post.update({
            PostField.board: board,
            PostField.author: post_author,
            PostField.list_date: list_date,
            PostField.post_status: post_del_status,
            PostField.pass_format_check: True
        })

        return post

    elif index == 0:

        lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index = \
            _api_util.parse_query_post(
                api,
                ori_screen)

        if lock_post:
            post.update({
                PostField.board: board,
                PostField.aid: post_aid,
                PostField.index: post_index,
                PostField.author: post_author,
                PostField.title: post_title,
                PostField.url: post_web,
                PostField.money: post_money,
                PostField.list_date: list_date,
                PostField.pass_format_check: True,
                PostField.push_number: push_number,
                PostField.is_lock: True})
            return post

    if query:
        post.update({
            PostField.board: board,
            PostField.aid: post_aid,
            PostField.index: post_index,
            PostField.author: post_author,
            PostField.title: post_title,
            PostField.url: post_web,
            PostField.money: post_money,
            PostField.list_date: list_date,
            PostField.pass_format_check: True,
            PostField.push_number: push_number})
        return post

    origin_post, has_control_code = _api_util.get_content(api)

    if origin_post is None:
        log.logger.info(i18n.post_deleted)

        post.update({
            PostField.board: board,
            PostField.aid: post_aid,
            PostField.index: post_index,
            PostField.author: post_author,
            PostField.title: post_title,
            PostField.url: post_web,
            PostField.money: post_money,
            PostField.list_date: list_date,
            PostField.has_control_code: has_control_code,
            PostField.pass_format_check: False,
            PostField.push_number: push_number,
            PostField.is_unconfirmed: api.Unconfirmed
        })
        return post

    post_author_pattern_new = re.compile('作者  (.+) 看板')
    post_author_pattern_old = re.compile('作者  (.+)')
    board_pattern = re.compile('看板  (.+)')

    post_date = None
    post_content = None
    ip = None
    location = None
    push_list = []

    # 格式確認,亂改的我也沒辦法Q_Q
    origin_post_lines = origin_post.split('\n')

    author_line = origin_post_lines[0]

    if board.lower() == 'allpost':
        board_line = author_line[author_line.find(')') + 1:]
        pattern_result = board_pattern.search(board_line)
        if pattern_result is not None:
            board_temp = post_author = pattern_result.group(0)
            board_temp = board_temp[2:].strip()
            if len(board_temp) > 0:
                board = board_temp
                log.logger.debug(i18n.board, board)

    pattern_result = post_author_pattern_new.search(author_line)
    if pattern_result is not None:
        post_author = pattern_result.group(0)
        post_author = post_author[:post_author.rfind(')') + 1]
    else:
        pattern_result = post_author_pattern_old.search(author_line)
        if pattern_result is None:
            log.logger.info(i18n.substandard_post, i18n.author)

            post.update({
                PostField.board: board,
                PostField.aid: post_aid,
                PostField.index: post_index,
                PostField.author: post_author,
                PostField.date: post_date,
                PostField.title: post_title,
                PostField.url: post_web,
                PostField.money: post_money,
                PostField.content: post_content,
                PostField.ip: ip,
                PostField.comments: push_list,
                PostField.list_date: list_date,
                PostField.has_control_code: has_control_code,
                PostField.pass_format_check: False,
                PostField.location: location,
                PostField.push_number: push_number,
                PostField.full_content: origin_post,
                PostField.is_unconfirmed: api.Unconfirmed, })

            return post
        post_author = pattern_result.group(0)
        post_author = post_author[:post_author.rfind(')') + 1]
    post_author = post_author[4:].strip()

    log.logger.debug(i18n.author, post_author)

    post_title_pattern = re.compile('標題  (.+)')

    title_line = origin_post_lines[1]
    pattern_result = post_title_pattern.search(title_line)
    if pattern_result is None:
        log.logger.info(i18n.substandard_post, i18n.title)

        post.update({
            PostField.board: board,
            PostField.aid: post_aid,
            PostField.index: post_index,
            PostField.author: post_author,
            PostField.date: post_date,
            PostField.title: post_title,
            PostField.url: post_web,
            PostField.money: post_money,
            PostField.content: post_content,
            PostField.ip: ip,
            PostField.comments: push_list,
            PostField.list_date: list_date,
            PostField.has_control_code: has_control_code,
            PostField.pass_format_check: False,
            PostField.location: location,
            PostField.push_number: push_number,
            PostField.full_content: origin_post,
            PostField.is_unconfirmed: api.Unconfirmed, })

        return post
    post_title = pattern_result.group(0)
    post_title = post_title[4:].strip()

    log.logger.debug(i18n.title, post_title)

    post_date_pattern = re.compile('時間  .{24}')
    date_line = origin_post_lines[2]
    pattern_result = post_date_pattern.search(date_line)
    if pattern_result is None:
        log.logger.info(i18n.substandard_post, i18n.date)

        post.update({
            PostField.board: board,
            PostField.aid: post_aid,
            PostField.index: post_index,
            PostField.author: post_author,
            PostField.date: post_date,
            PostField.title: post_title,
            PostField.url: post_web,
            PostField.money: post_money,
            PostField.content: post_content,
            PostField.ip: ip,
            PostField.comments: push_list,
            PostField.list_date: list_date,
            PostField.has_control_code: has_control_code,
            PostField.pass_format_check: False,
            PostField.location: location,
            PostField.push_number: push_number,
            PostField.full_content: origin_post,
            PostField.is_unconfirmed: api.Unconfirmed, })

        return post
    post_date = pattern_result.group(0)
    post_date = post_date[4:].strip()

    log.logger.debug(i18n.date, post_date)

    content_fail = True
    if screens.Target.content_start not in origin_post:
        # print('Type 1')
        content_fail = True
    else:
        post_content = origin_post
        post_content = post_content[
                       post_content.find(screens.Target.content_start) + len(screens.Target.content_start) + 1:]
        # print('Type 2')
        # print(f'PostContent [{PostContent}]')
        for content_end in screens.Target.content_end_list:
            # + 3 = 把 --\n 拿掉
            # print(f'EC [{EC}]')
            if content_end in post_content:
                content_fail = False

                post_content = post_content[:post_content.rfind(content_end) + 3]
                origin_post_lines = origin_post[origin_post.find(content_end):]
                # post_content = post_content.strip()
                origin_post_lines = origin_post_lines.split('\n')
                break

    if content_fail:
        log.logger.info(i18n.substandard_post, i18n.content)

        post.update({
            PostField.board: board,
            PostField.aid: post_aid,
            PostField.index: post_index,
            PostField.author: post_author,
            PostField.date: post_date,
            PostField.title: post_title,
            PostField.url: post_web,
            PostField.money: post_money,
            PostField.content: post_content,
            PostField.ip: ip,
            PostField.comments: push_list,
            PostField.list_date: list_date,
            PostField.has_control_code: has_control_code,
            PostField.pass_format_check: False,
            PostField.location: location,
            PostField.push_number: push_number,
            PostField.full_content: origin_post,
            PostField.is_unconfirmed: api.Unconfirmed, })

        return post

    log.logger.debug(i18n.content, post_content)

    info_lines = [line for line in origin_post_lines if line.startswith('※') or line.startswith('◆')]

    pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+')
    pattern_p2 = re.compile('[\d]+-[\d]+-[\d]+-[\d]+')
    for line in reversed(info_lines):

        log.logger.debug('IP Line', line)

        # type 1
        # ※ 編輯: CodingMan (111.243.146.98 臺灣)
        # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 111.243.146.98 (臺灣)

        # type 2
        # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 116.241.32.178
        # ※ 編輯: kill77845 (114.136.55.237), 12/08/2018 16:47:59

        # type 3
        # ※ 發信站: 批踢踢實業坊(ptt.cc)
        # ◆ From: 211.20.78.69
        # ※ 編輯: JCC             來自: 211.20.78.69         (06/20 10:22)
        # ※ 編輯: JCC (118.163.28.150), 12/03/2015 14:25:35

        pattern_result = pattern.search(line)
        if pattern_result is not None:
            ip = pattern_result.group(0)
            location_temp = line[line.find(ip) + len(ip):].strip()
            location_temp = location_temp.replace('(', '')
            location_temp = location_temp[:location_temp.rfind(')')]
            location_temp = location_temp.strip()
            # print(f'=>[{LocationTemp}]')
            if ' ' not in location_temp and len(location_temp) > 0:
                location = location_temp

                log.logger.debug('Location', location)
            break

        pattern_result = pattern_p2.search(line)
        if pattern_result is not None:
            ip = pattern_result.group(0)
            ip = ip.replace('-', '.')
            # print(f'IP -> [{IP}]')
            break
    if api.config.host == data_type.HOST.PTT1:
        if ip is None:
            log.logger.info(i18n.substandard_post, ip)

            post.update({
                PostField.board: board,
                PostField.aid: post_aid,
                PostField.index: post_index,
                PostField.author: post_author,
                PostField.date: post_date,
                PostField.title: post_title,
                PostField.url: post_web,
                PostField.money: post_money,
                PostField.content: post_content,
                PostField.ip: ip,
                PostField.comments: push_list,
                PostField.list_date: list_date,
                PostField.has_control_code: has_control_code,
                PostField.pass_format_check: False,
                PostField.location: location,
                PostField.push_number: push_number,
                PostField.full_content: origin_post,
                PostField.is_unconfirmed: api.Unconfirmed, })

            return post
    log.logger.debug('IP', ip)

    push_author_pattern = re.compile('[推|噓|→] [\w| ]+:')
    push_date_pattern = re.compile('[\d]+/[\d]+ [\d]+:[\d]+')
    push_ip_pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+')

    push_list = []

    for line in origin_post_lines:
        if line.startswith('推'):
            comment_type = data_type.CommentType.PUSH
        elif line.startswith('噓 '):
            comment_type = data_type.CommentType.BOO
        elif line.startswith('→ '):
            comment_type = data_type.CommentType.ARROW
        else:
            continue

        result = push_author_pattern.search(line)
        if result is None:
            # 不符合推文格式
            continue
        push_author = result.group(0)[2:-1].strip()

        log.logger.debug(i18n.comment_id, push_author)

        result = push_date_pattern.search(line)
        if result is None:
            continue
        push_date = result.group(0)
        log.logger.debug(i18n.comment_date, push_date)

        comment_ip = None
        result = push_ip_pattern.search(line)
        if result is not None:
            comment_ip = result.group(0)
            log.logger.debug(f'{i18n.comment} ip', comment_ip)

        push_content = line[line.find(push_author) + len(push_author):]
        # PushContent = PushContent.replace(PushDate, '')

        if api.config.host == data_type.HOST.PTT1:
            push_content = push_content[:push_content.rfind(push_date)]
        else:
            # → CodingMan:What is Ptt?                                       推 10/04 13:25
            push_content = push_content[:push_content.rfind(push_date) - 2]
        if comment_ip is not None:
            push_content = push_content.replace(comment_ip, '')
        push_content = push_content[push_content.find(':') + 1:].strip()

        log.logger.debug(i18n.comment_content, push_content)

        current_push = {
            CommentField.type: comment_type,
            CommentField.author: push_author,
            CommentField.content: push_content,
            CommentField.ip: comment_ip,
            CommentField.time: push_date}
        push_list.append(current_push)

    post.update({
        PostField.board: board,
        PostField.aid: post_aid,
        PostField.index: post_index,
        PostField.author: post_author,
        PostField.date: post_date,
        PostField.title: post_title,
        PostField.url: post_web,
        PostField.money: post_money,
        PostField.content: post_content,
        PostField.ip: ip,
        PostField.comments: push_list,
        PostField.list_date: list_date,
        PostField.has_control_code: has_control_code,
        PostField.pass_format_check: True,
        PostField.location: location,
        PostField.push_number: push_number,
        PostField.full_content: origin_post,
        PostField.is_unconfirmed: api.Unconfirmed})

    return post


================================================
FILE: PyPtt/_api_get_post_index.py
================================================
from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens


def get_post_index(api, board: str, aid: str) -> int:
    _api_util.goto_board(api, board)

    cmd_list = []
    cmd_list.append('#')
    cmd_list.append(aid)
    cmd_list.append(command.enter)

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('找不到這個文章代碼', log_level=log.DEBUG,
                                exceptions_=exceptions.NoSuchPost(board, aid)),
        # 此狀態下無法使用搜尋文章代碼(AID)功能
        connect_core.TargetUnit('此狀態下無法使用搜尋文章代碼(AID)功能',
                                exceptions_=exceptions.CanNotUseSearchPostCode()),
        connect_core.TargetUnit('沒有文章...', exceptions_=exceptions.NoSuchPost(board, aid)),
        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit(screens.Target.InBoardWithCursor, log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board))
    ]

    index = api.connect_core.send(
        cmd,
        target_list
    )
    ori_screen = api.connect_core.get_screen_queue()[-1]
    if index < 0:
        # print(OriScreen)
        raise exceptions.NoSuchBoard(api.config, board)

    # if index == 5:
    #     print(OriScreen)
    #     raise exceptions.NoSuchBoard(api.config, board)

    # print(index)
    # print(OriScreen)
    screen_list = ori_screen.split('\n')

    line = [x for x in screen_list if x.startswith(api.cursor)]
    line = line[0]
    last_line = screen_list[screen_list.index(line) - 1]
    # print(LastLine)
    # print(line)

    if '編號' in last_line and '人氣:' in last_line:
        index = line[1:].strip()
        index_fix = False
    else:
        index = last_line.strip()
        index_fix = True
    while '  ' in index:
        index = index.replace('  ', ' ')
    index_list = index.split(' ')
    index = index_list[0]
    if index == '★':
        return 0
    index = int(index)
    if index_fix:
        index += 1
    # print(Index)
    return index


================================================
FILE: PyPtt/_api_get_time.py
================================================
import re

from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens

pattern = re.compile('[\d]+:[\d][\d]')


def get_time(api) -> str:
    _api_util.one_thread(api)
    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append('A')
    cmd_list.append(command.right)
    cmd_list.append(command.left)

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
    ]

    index = api.connect_core.send(cmd, target_list)
    if index != 0:
        return None

    ori_screen = api.connect_core.get_screen_queue()[-1]
    line_list = ori_screen.split('\n')[-3:]

    # 0:00

    for line in line_list:
        if '星期' in line and '線上' in line and '我是' in line:
            result = pattern.search(line)
            if result is not None:
                return result.group(0)
    return None


================================================
FILE: PyPtt/_api_get_user.py
================================================
import json
import re
from typing import Dict

from AutoStrEnum import AutoJsonEncoder

from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
from .data_type import UserField


def get_user(api, ptt_id: str) -> Dict:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    check_value.check_type(ptt_id, str, 'UserID')
    if len(ptt_id) < 2:
        raise ValueError(f'wrong parameter user_id: {ptt_id}')

    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append('T')
    cmd_list.append(command.enter)
    cmd_list.append('Q')
    cmd_list.append(command.enter)
    cmd_list.append(ptt_id)
    cmd_list.append(command.enter)

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit(screens.Target.AnyKey, break_detect=True),
        connect_core.TargetUnit(screens.Target.InTalk, break_detect=True),
    ]

    index = api.connect_core.send(
        cmd,
        target_list)
    ori_screen = api.connect_core.get_screen_queue()[-1]
    if index == 1:
        raise exceptions.NoSuchUser(ptt_id)
    # PTT1
    # 《ID暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》小康 ($73866)
    # 《登入次數》1118 次 (同天內只計一次) 《有效文章》15 篇 (退:0)
    # 《目前動態》閱讀文章     《私人信箱》最近無新信件
    # 《上次上站》10/06/2019 17:29:49 Sun  《上次故鄉》111.251.231.184
    # 《 五子棋 》 0 勝  0 敗  0 和 《象棋戰績》 0 勝  0 敗  0 和

    # https://github.com/Truth0906/PTTLibrary

    # 強大的 PTT 函式庫
    # 提供您 快速 穩定 完整 的 PTT API

    # 提供專業的 PTT 機器人諮詢服務

    # PTT2
    # 《ID暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》家徒四壁 ($0)
    # 《登入次數》8 次 (同天內只計一次)  《有效文章》0 篇
    # 《目前動態》看板列表     《私人信箱》最近無新信件
    # 《上次上站》10/06/2019 17:27:55 Sun  《上次故鄉》111.251.231.184
    # 《 五子棋 》 0 勝  0 敗  0 和 《象棋戰績》 0 勝  0 敗  0 和

    # 《個人名片》CodingMan 目前沒有名片

    lines = ori_screen.split('\n')[1:]

    def parse_user_info_from_line(line: str) -> (str, str):
        part_0 = line[line.find('》') + 1:]
        part_0 = part_0[:part_0.find('《')].strip()

        part_1 = line[line.rfind('》') + 1:].strip()

        return part_0, part_1

    ptt_id, buff_1 = parse_user_info_from_line(lines[0])
    money = int(int_list[0]) if len(int_list := re.findall(r'\d+', buff_1)) > 0 else buff_1
    buff_0, buff_1 = parse_user_info_from_line(lines[1])

    login_count = int(re.findall(r'\d+', buff_0)[0])
    account_verified = ('同天內只計一次' in buff_0)
    legal_post = int(re.findall(r'\d+', buff_1)[0])

    # PTT2 沒有退文
    if api.config.host == data_type.HOST.PTT1:
        illegal_post = int(re.findall(r'\d+', buff_1)[1])
    else:
        illegal_post = None

    activity, mail = parse_user_info_from_line(lines[2])
    last_login_date, last_login_ip = parse_user_info_from_line(lines[3])
    five_chess, chess = parse_user_info_from_line(lines[4])

    signature_file = '\n'.join(lines[5:-1]).strip('\n')

    log.logger.debug('ptt_id', ptt_id)
    log.logger.debug('money', money)
    log.logger.debug('login_count', login_count)
    log.logger.debug('account_verified', account_verified)
    log.logger.debug('legal_post', legal_post)
    log.logger.debug('illegal_post', illegal_post)
    log.logger.debug('activity', activity)
    log.logger.debug('mail', mail)
    log.logger.debug('last_login_date', last_login_date)
    log.logger.debug('last_login_ip', last_login_ip)
    log.logger.debug('five_chess', five_chess)
    log.logger.debug('chess', chess)
    log.logger.debug('signature_file', signature_file)

    user = {
        UserField.ptt_id: ptt_id,
        UserField.money: money,
        UserField.login_count: login_count,
        UserField.account_verified: account_verified,
        UserField.legal_post: legal_post,
        UserField.illegal_post: illegal_post,
        UserField.activity: activity,
        UserField.mail: mail,
        UserField.last_login_date: last_login_date,
        UserField.last_login_ip: last_login_ip,
        UserField.five_chess: five_chess,
        UserField.chess: chess,
        UserField.signature_file: signature_file,
    }
    user = json.dumps(user, cls=AutoJsonEncoder)
    return json.loads(user)


================================================
FILE: PyPtt/_api_give_money.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import log


def give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag_content: str) -> None:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    check_value.check_type(ptt_id, str, 'ptt_id')
    check_value.check_type(money, int, 'money')

    if red_bag_title is not None:
        check_value.check_type(red_bag_title, str, 'red_bag_title')
    else:
        red_bag_title = ''

    if red_bag_content is not None:
        check_value.check_type(red_bag_content, str, 'red_bag_content')
    else:
        red_bag_content = ''

    log.logger.info(
        i18n.replace(i18n.give_money_to, ptt_id, money))

    # Check data_type.user
    api.get_user(ptt_id)

    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append('P')
    cmd_list.append(command.enter)
    cmd_list.append('P')
    cmd_list.append(command.enter)
    cmd_list.append('O')
    cmd_list.append(command.enter)

    cmd = ''.join(cmd_list)

    edit_red_bag_cmd_list = list()

    edit_red_bag_target = connect_core.TargetUnit('要修改紅包袋嗎', response='n' + command.enter)
    if red_bag_title != '' or red_bag_content != '':
        edit_red_bag_cmd_list.append('y')
        edit_red_bag_cmd_list.append(command.enter)
        if red_bag_title != '':
            edit_red_bag_cmd_list.append(command.down)
            edit_red_bag_cmd_list.append(command.ctrl_y)  # remove the red_bag_title
            edit_red_bag_cmd_list.append(command.enter)
            edit_red_bag_cmd_list.append(command.up)
            edit_red_bag_cmd_list.append(f'標題: {red_bag_title}')
            # reset cursor to original position
            edit_red_bag_cmd_list.append(command.up * 2)
        if red_bag_content != '':
            edit_red_bag_cmd_list.append(command.down * 4)
            edit_red_bag_cmd_list.append(command.ctrl_y * 8)  # remove original red_bag_content
            edit_red_bag_cmd_list.append(red_bag_content)
        edit_red_bag_cmd_list.append(command.ctrl_x)

        edit_red_bag_cmd = ''.join(edit_red_bag_cmd_list)
        edit_red_bag_target = connect_core.TargetUnit('要修改紅包袋嗎', response=edit_red_bag_cmd)

    target_list = [
        connect_core.TargetUnit('你沒有那麼多Ptt幣喔!', break_detect=True, exceptions_=exceptions.NoMoney()),
        connect_core.TargetUnit('金額過少,交易取消!', break_detect=True, exceptions_=exceptions.NoMoney()),
        connect_core.TargetUnit('交易取消!', break_detect=True,
                                exceptions_=exceptions.UnknownError(i18n.transaction_cancelled)),
        connect_core.TargetUnit('確定進行交易嗎?', response='y' + command.enter),
        connect_core.TargetUnit('按任意鍵繼續', break_detect=True),
        edit_red_bag_target,
        connect_core.TargetUnit('要修改紅包袋嗎', response=command.enter),
        connect_core.TargetUnit('完成交易前要重新確認您的身份', response=api._ptt_pw + command.enter),
        connect_core.TargetUnit('他是你的小主人,是否匿名?', response='n' + command.enter),
        connect_core.TargetUnit('要給他多少Ptt幣呢?', response=command.tab + str(money) + command.enter),
        connect_core.TargetUnit('這位幸運兒的id', response=ptt_id + command.enter),
        connect_core.TargetUnit('認證尚未過期', response='y' + command.enter),
        connect_core.TargetUnit('交易正在進行中', response=command.space)
    ]

    api.connect_core.send(
        cmd,
        target_list,
        screen_timeout=api.config.screen_long_timeout
    )

    log.logger.info(
        i18n.replace(i18n.give_money_to, ptt_id, money),
        '...', i18n.success)


================================================
FILE: PyPtt/_api_has_new_mail.py
================================================
import re

from . import _api_util
from . import command
from . import connect_core
from . import log
from . import screens


def has_new_mail(api) -> int:
    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append(command.ctrl_z)
    cmd_list.append('m')
    # cmd_list.append('1')
    # cmd_list.append(command.enter)
    cmd = ''.join(cmd_list)
    current_capacity = None
    plus_count = 0
    index_pattern = re.compile('(\d+)')
    checked_index_list = []
    break_detect = False

    target_list = [
        connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True)
    ]

    api.connect_core.send(
        cmd,
        target_list,
    )
    current_capacity, _ = _api_util.get_mailbox_capacity(api)
    if current_capacity > 20:
        cmd_list = []
        cmd_list.append(command.go_main_menu)
        cmd_list.append(command.ctrl_z)
        cmd_list.append('m')
        cmd_list.append('1')
        cmd_list.append(command.enter)
        cmd = ''.join(cmd_list)

    while True:
        if current_capacity > 20:
            api.connect_core.send(
                cmd,
                target_list,
            )
        last_screen = api.connect_core.get_screen_queue()[-1]

        last_screen_list = last_screen.split('\n')
        last_screen_list = last_screen_list[3:-1]
        last_screen_list = [x[:10] for x in last_screen_list]

        current_plus_count = 0
        for line in last_screen_list:
            if str(current_capacity) in line:
                break_detect = True

            index_result = index_pattern.search(line)
            if index_result is None:
                continue
            current_index = index_result.group(0)
            if current_index in checked_index_list:
                continue
            checked_index_list.append(current_index)
            if '+' not in line:
                continue

            current_plus_count += 1

        plus_count += current_plus_count
        if break_detect:
            break
        cmd = command.ctrl_f

    return plus_count


================================================
FILE: PyPtt/_api_loginout.py
================================================
import re

from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import log
from . import screens


def logout(api) -> None:
    _api_util.one_thread(api)

    log.logger.info(i18n.logout)
    if not api._is_login:
        log.logger.info(i18n.logout, '...', i18n.success)
        return

    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append('g')
    cmd_list.append(command.enter)
    cmd_list.append('y')
    cmd_list.append(command.enter)

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('任意鍵', break_detect=True),
    ]

    try:
        api.connect_core.send(cmd, target_list)
        api.connect_core.close()
    except exceptions.ConnectionClosed:
        pass
    except RuntimeError:
        pass

    api._is_login = False

    log.logger.info(i18n.logout, '...', i18n.success)


def login(api, ptt_id: str, ptt_pw: str, kick_other_session: bool):
    _api_util.one_thread(api)

    check_value.check_type(ptt_id, str, 'ptt_id')
    check_value.check_type(ptt_pw, str, 'password')
    check_value.check_type(kick_other_session, bool, 'kick_other_session')

    if api._is_login:
        api.logout()

    api.config.kick_other_session = kick_other_session

    def kick_other_login_display_msg():
        if api.config.kick_other_session:
            return i18n.kick_other_login
        return i18n.not_kick_other_login

    def kick_other_login_response(screen):
        if api.config.kick_other_session:
            return 'y' + command.enter
        return 'n' + command.enter

    api.is_mailbox_full = False

    # def is_mailbox_full():
    #     log.log(
    #         api.config,
    #         LogLevel.INFO,
    #         i18n.MailBoxFull)
    #     api.is_mailbox_full = True

    def register_processing(screen):
        pattern = re.compile('[\d]+')
        api.process_picks = int(pattern.search(screen).group(0))

    if len(ptt_pw) > 8:
        ptt_pw = ptt_pw[:8]

    ptt_id = ptt_id.strip()
    ptt_pw = ptt_pw.strip()

    api.ptt_id = ptt_id
    api._ptt_pw = ptt_pw

    api.connect_core.connect()

    log.logger.info(i18n.login_id, ptt_id)

    target_list = [
        connect_core.TargetUnit(screens.Target.InMailBox,
                                response=command.go_main_menu + 'A' + command.right + command.left, break_detect=True),
        connect_core.TargetUnit(screens.Target.InMailMenu, response=command.go_main_menu),
        connect_core.TargetUnit(screens.Target.MainMenu, break_detect=True),
        connect_core.TargetUnit('【看板列表】', response=command.go_main_menu),
        connect_core.TargetUnit('密碼不對', break_detect=True, exceptions_=exceptions.WrongIDorPassword()),
        connect_core.TargetUnit('請重新輸入', break_detect=True, exceptions_=exceptions.WrongIDorPassword()),
        connect_core.TargetUnit('登入太頻繁', response=' '),
        connect_core.TargetUnit('系統過載', break_detect=True),
        connect_core.TargetUnit('您要刪除以上錯誤嘗試的記錄嗎', response='y' + command.enter),
        connect_core.TargetUnit('請選擇暫存檔 (0-9)[0]', response=command.enter),
        connect_core.TargetUnit('有一篇文章尚未完成', response='Q' + command.enter),
        connect_core.TargetUnit(
            '請重新設定您的聯絡信箱', break_detect=True, exceptions_=exceptions.ResetYourContactEmail()),
        # connect_core.TargetUnit(
        #     i18n.in_login_process_please_wait,
        #     '登入中,請稍候'),
        connect_core.TargetUnit('密碼正確'),
        # 密碼正確
        connect_core.TargetUnit('您想刪除其他重複登入的連線嗎', response=kick_other_login_response),
        connect_core.TargetUnit('◆ 您的註冊申請單尚在處理中', response=command.enter, handler=register_processing),
        connect_core.TargetUnit('任意鍵', response=' '),
        connect_core.TargetUnit('正在更新與同步線上使用者及好友名單'),
        connect_core.TargetUnit('【分類看板】', response=command.go_main_menu),
        connect_core.TargetUnit([
            '大富翁',
            '排行榜',
            '名次',
            '代號',
            '暱稱',
            '數目'
        ], response=command.go_main_menu),
        connect_core.TargetUnit([
            '熱門話題'
        ], response=command.go_main_menu),
        connect_core.TargetUnit('您確定要填寫註冊單嗎', response=command.enter * 3),
        connect_core.TargetUnit('以上資料是否正確', response='y' + command.enter),
        connect_core.TargetUnit('另外若輸入後發生認證碼錯誤請先確認輸入是否為最後一封', response='x' + command.enter),
        connect_core.TargetUnit('此帳號已設定為只能使用安全連線', exceptions_=exceptions.OnlySecureConnection())
    ]

    # IAC = '\xff'
    # WILL = '\xfb'
    # NAWS = '\x1f'

    cmd_list = []
    # cmd_list.append(IAC + WILL + NAWS)
    cmd_list.append(ptt_id + ',')
    cmd_list.append(command.enter)
    cmd_list.append(ptt_pw)
    cmd_list.append(command.enter)

    cmd = ''.join(cmd_list)

    index = api.connect_core.send(
        cmd,
        target_list,
        screen_timeout=api.config.screen_long_timeout,
        refresh=False,
        secret=True)
    if index == 0:

        current_capacity, max_capacity = _api_util.get_mailbox_capacity(api)

        log.logger.info(i18n.has_new_mail_goto_main_menu)

        if current_capacity > max_capacity:
            api.is_mailbox_full = True

            log.logger.info(i18n.mail_box_full)

        if api.is_mailbox_full:
            log.logger.info(i18n.use_mailbox_api_will_logout_after_execution)

        target_list = [
            connect_core.TargetUnit(screens.Target.MainMenu, break_detect=True)
        ]

        cmd = command.go_main_menu + 'A' + command.right + command.left

        index = api.connect_core.send(
            cmd,
            target_list,
            screen_timeout=api.config.screen_long_timeout,
            secret=True)

    ori_screen = api.connect_core.get_screen_queue()[-1]

    is_login = True

    for t in screens.Target.MainMenu:
        if t not in ori_screen:
            is_login = False
            break

    if not is_login:
        raise exceptions.LoginError()

    if '> (' in ori_screen:
        api.cursor = data_type.Cursor.NEW
        log.logger.debug(i18n.new_cursor)
    elif '●(' in ori_screen:
        api.cursor = data_type.Cursor.OLD
        log.logger.debug(i18n.old_cursor)
    else:
        raise exceptions.UnknownError()

    screens.Target.InBoardWithCursor = screens.Target.InBoardWithCursor[:screens.Target.InBoardWithCursorLen]
    screens.Target.InBoardWithCursor.append(api.cursor)

    screens.Target.InMailBoxWithCursor = screens.Target.InMailBoxWithCursor[:screens.Target.InMailBoxWithCursorLen]
    screens.Target.InMailBoxWithCursor.append(api.cursor)

    screens.Target.CursorToGoodbye = screens.Target.CursorToGoodbye[:len(screens.Target.MainMenu)]
    if api.cursor == '>':
        screens.Target.CursorToGoodbye.append('> (G)oodbye')
    else:
        screens.Target.CursorToGoodbye.append('●(G)oodbye')

    unregistered_user = True
    if '(T)alk' in ori_screen:
        unregistered_user = False
    if '(P)lay' in ori_screen:
        unregistered_user = False
    if '(N)amelist' in ori_screen:
        unregistered_user = False

    if unregistered_user:
        log.logger.info(i18n.unregistered_user_cant_use_all_api)

    api.is_registered_user = not unregistered_user

    if api.process_picks != 0:
        log.logger.info(i18n.picks_in_register, api.process_picks)

    api._is_login = True
    log.logger.info(i18n.login_success)


================================================
FILE: PyPtt/_api_mail.py
================================================
from __future__ import annotations

import re
from typing import Dict, Optional

from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
from .data_type import MailField


# 寄信
def mail(api,
         ptt_id: str,
         title: str,
         content: str,
         sign_file,
         backup: bool = True) -> None:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    check_value.check_type(ptt_id, str, 'ptt_id')
    check_value.check_type(title, str, 'title')
    check_value.check_type(content, str, 'content')

    api.get_user(ptt_id)

    check_sign_file = False
    for i in range(0, 10):
        if str(i) == sign_file or i == sign_file:
            check_sign_file = True
            break

    if not check_sign_file:
        if sign_file.lower() != 'x':
            raise ValueError(f'wrong parameter sign_file: {sign_file}')

    cmd_list = []
    # 回到主選單
    cmd_list.append(command.go_main_menu)
    # 私人信件區
    cmd_list.append('M')
    cmd_list.append(command.enter)
    # 站內寄信
    cmd_list.append('S')
    cmd_list.append(command.enter)
    # 輸入 id
    cmd_list.append(ptt_id)
    cmd_list.append(command.enter)

    cmd = ''.join(cmd_list)

    # 定義如何根據情況回覆訊息
    target_list = [
        connect_core.TargetUnit('主題:', break_detect=True),
        connect_core.TargetUnit('【電子郵件】', exceptions_=exceptions.NoSuchUser(ptt_id))
    ]

    api.connect_core.send(
        cmd,
        target_list,
        screen_timeout=api.config.screen_long_timeout
    )

    cmd_list = []
    # 輸入標題
    cmd_list.append(title)
    cmd_list.append(command.enter)
    # 輸入內容
    cmd_list.append(content)
    # 儲存檔案
    cmd_list.append(command.ctrl_x)

    cmd = ''.join(cmd_list)

    # 定義如何根據情況回覆訊息
    target_list = [
        connect_core.TargetUnit('請按任意鍵繼續', response=command.enter, break_detect_after_send=True),
        connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
        connect_core.TargetUnit('是否自存底稿', response=('y' if backup else 'n') + command.enter),
        connect_core.TargetUnit('選擇簽名檔', response=str(sign_file) + command.enter),
        connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),
    ]

    # 送出訊息
    api.connect_core.send(
        cmd,
        target_list,
        screen_timeout=api.config.screen_post_timeout)

    log.logger.info(i18n.send_mail, i18n.success)


# --
# ※ 發信站: 批踢踢實業坊(ptt.cc)
# ◆ From: 220.142.14.95
content_start = '───────────────────────────────────────'
content_end = '--\n※ 發信站: 批踢踢實業坊(ptt.cc)'
content_ip_old = '◆ From: '

mail_author_pattern = re.compile('作者  (.+)')
mail_title_pattern = re.compile('標題  (.+)')
mail_date_pattern = re.compile('時間  (.+)')
ip_pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+')


def get_mail(api, index: int, search_type: Optional[data_type.SearchType] = None,
             search_condition: Optional[str] = None,
             search_list: Optional[list] = None) -> Dict:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    log.logger.info(i18n.get_mail)

    if not isinstance(index, int):
        raise ValueError('index must be int')

    current_index = api.get_newest_index(data_type.NewIndex.MAIL)
    if index <= 0 or current_index < index:
        raise exceptions.NoSuchMail()
    # check_value.check_index('index', index, current_index)

    cmd_list = []
    # 回到主選單
    cmd_list.append(command.go_main_menu)
    # 進入信箱
    cmd_list.append(command.ctrl_z)
    cmd_list.append('m')

    # 處理條件整理出指令
    _cmd_list = _api_util.get_search_condition_cmd(data_type.NewIndex.MAIL, search_list)

    cmd_list.extend(_cmd_list)

    # 前進至目標信件位置
    cmd_list.append(str(index))
    cmd_list.append(command.enter)
    cmd = ''.join(cmd_list)

    # 有時候會沒有最底下一列,只好偵測游標是否出現
    if api.cursor == data_type.Cursor.NEW:
        space_length = 6 - len(api.cursor) - len(str(index))
    else:
        space_length = 5 - len(api.cursor) - len(str(index))
    fast_target = f"{api.cursor}{' ' * space_length}{index}"

    # 定義如何根據情況回覆訊息
    target_list = [
        connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit(fast_target, log_level=log.DEBUG, break_detect=True)
    ]

    # 送出訊息
    api.connect_core.send(
        cmd,
        target_list)

    # 取得信件全文
    origin_mail, _ = _api_util.get_content(api, post_mode=False)

    # 使用表示式分析信件作者
    pattern_result = mail_author_pattern.search(origin_mail)
    if pattern_result is None:
        mail_author = None
    else:
        mail_author = pattern_result.group(0)[2:].strip()

    # 使用表示式分析信件標題
    pattern_result = mail_title_pattern.search(origin_mail)
    if pattern_result is None:
        mail_title = None
    else:
        mail_title = pattern_result.group(0)[2:].strip()

    # 使用表示式分析信件日期
    pattern_result = mail_date_pattern.search(origin_mail)
    if pattern_result is None:
        mail_date = None
    else:
        mail_date = pattern_result.group(0)[2:].strip()

    # 從全文拿掉信件開頭作為信件內文
    mail_content = origin_mail[origin_mail.find(content_start) + len(content_start) + 1:]

    mail_content = mail_content[
                   mail_content.find(screens.Target.content_start) + len(screens.Target.content_start) + 1:]

    for EC in screens.Target.content_end_list:
        # + 3 = 把 --\n 拿掉
        if EC in mail_content:
            mail_content = mail_content[:mail_content.rfind(EC) + 3].rstrip()
            break

    # 紅包偵測
    red_envelope = False
    if content_end not in origin_mail and 'Ptt幣的大紅包喔' in origin_mail:
        mail_content = mail_content.strip()
        red_envelope = True
    else:

        mail_content = mail_content[:mail_content.rfind(content_end) + 3]

    if red_envelope:
        mail_ip = None
        mail_location = None
    else:
        # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 111.242.182.114
        # ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 59.104.127.126 (臺灣)

        # 非紅包開始解析 ip 與 地區

        ip_line_list = origin_mail.split('\n')
        ip_line = [x for x in ip_line_list if x.startswith(content_end[3:])]

        if len(ip_line) == 0:
            # 沒 ip 就沒地區
            mail_ip = None
            mail_location = None
        else:
            ip_line = ip_line[0]

            result = ip_pattern.search(ip_line)
            if result is None:
                ip_line = [x for x in ip_line_list if x.startswith(content_ip_old)]

                if len(ip_line) == 0:
                    mail_ip = None
                else:
                    ip_line = ip_line[0]
                    result = ip_pattern.search(ip_line)
                    mail_ip = result.group(0)
            else:
                mail_ip = result.group(0)

            location = ip_line[ip_line.find(mail_ip) + len(mail_ip):].strip()
            if len(location) == 0:
                mail_location = None
            else:
                # print(location)
                mail_location = location[1:-1]

    log.logger.info(i18n.get_mail, '...', i18n.success)

    return {
        MailField.origin_mail: origin_mail,
        MailField.author: mail_author,
        MailField.title: mail_title,
        MailField.date: mail_date,
        MailField.content: mail_content,
        MailField.ip: mail_ip,
        MailField.location: mail_location,
        MailField.is_red_envelope: red_envelope}


def del_mail(api, index) -> None:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    current_index = api.get_newest_index(data_type.NewIndex.MAIL)
    check_value.check_index(index, current_index)

    cmd_list = []
    # 進入主選單
    cmd_list.append(command.go_main_menu)
    # 進入信箱
    cmd_list.append(command.ctrl_z)
    cmd_list.append('m')
    if index > 20:
        # speed up
        cmd_list.append(str(1))
        cmd_list.append(command.enter)

    # 前進到目標信件位置
    cmd_list.append(str(index))
    cmd_list.append(command.enter)
    # 刪除
    cmd_list.append('dy')
    cmd_list.append(command.enter)
    cmd = ''.join(cmd_list)

    # 定義如何根據情況回覆訊息
    target_list = [
        connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True)
    ]

    # 送出
    api.connect_core.send(
        cmd,
        target_list)

    if api.is_mailbox_full:
        api.logout()
        raise exceptions.MailboxFull()


================================================
FILE: PyPtt/_api_mark_post.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens


def mark_post(api, mark_type: int, board: str, post_aid: str, post_index: int, search_type: int,
              search_condition: str) -> None:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    if not isinstance(mark_type, data_type.MarkType):
        raise TypeError(f'mark_type must be data_type.MarkType')

    check_value.check_type(board, str, 'board')
    if post_aid is not None:
        check_value.check_type(post_aid, str, 'PostAID')
    check_value.check_type(post_index, int, 'PostIndex')

    if not isinstance(search_type, data_type.SearchType):
        raise TypeError(f'search_type must be data_type.SearchType')

    if search_condition is not None:
        check_value.check_type(search_condition, str, 'SearchCondition')

    if len(board) == 0:
        raise ValueError(f'board error parameter: {board}')

    if mark_type != data_type.MarkType.DELETE_D:
        if post_index != 0 and isinstance(post_aid, str):
            raise ValueError('wrong parameter index and aid can\'t both input')

        if post_index == 0 and post_aid is None:
            raise ValueError('wrong parameter index or aid must input')

    if search_condition is not None and search_type == 0:
        raise ValueError('wrong parameter index or aid must input')

    if search_type == data_type.SearchType.COMMENT:
        try:
            S = int(search_condition)
        except ValueError:
            raise ValueError(f'wrong parameter search_condition: {search_condition}')

        check_value.check_range(S, -100, 100, 'search_condition')

    if post_aid is not None and search_condition is not None:
        raise ValueError('wrong parameter aid and search_condition can\'t both input')

    if post_index != 0:
        newest_index = api.get_newest_index(
            data_type.NewIndex.BOARD,
            board=board,
            search_type=search_type,
            search_condition=search_condition)
        check_value.check_index(
            'index',
            post_index,
            max_value=newest_index)

    if mark_type == data_type.MarkType.UNCONFIRMED:
        # 批踢踢兔沒有待證文章功能 QQ
        if api.config.host == data_type.HOST.PTT2:
            raise exceptions.HostNotSupport(lib_util.get_current_func_name())

    _api_util.check_board(
        board,
        check_moderator=True)

    _api_util.goto_board(api, board)

    cmd_list = []
    if post_aid is not None:
        cmd_list.append(lib_util.check_aid(post_aid))
        cmd_list.append(command.enter)
    elif post_index != 0:
        if search_condition is not None:
            if search_type == data_type.SearchType.KEYWORD:
                cmd_list.append('/')
            elif search_type == data_type.SearchType.AUTHOR:
                cmd_list.append('a')
            elif search_type == data_type.SearchType.COMMENT:
                cmd_list.append('Z')
            elif search_type == data_type.SearchType.MARK:
                cmd_list.append('G')
            elif search_type == data_type.SearchType.MONEY:
                cmd_list.append('A')

            cmd_list.append(search_condition)
            cmd_list.append(command.enter)

        cmd_list.append(str(post_index))

        cmd_list.append(command.enter)
    else:
        raise ValueError('post_aid and post_index cannot be None at the same time')

    if mark_type == data_type.MarkType.S:
        cmd_list.append('L')
    elif mark_type == data_type.MarkType.D:
        cmd_list.append('t')
    elif mark_type == data_type.MarkType.DELETE_D:
        cmd_list.append(command.ctrl_d)
    elif mark_type == data_type.MarkType.M:
        cmd_list.append('m')
    elif mark_type == data_type.MarkType.UNCONFIRMED:
        cmd_list.append(command.ctrl_e + 'S')

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('刪除所有標記', log_level=log.INFO, response='y' + command.enter),
        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.INFO, break_detect=True),
    ]

    api.connect_core.send(cmd, target_list)


================================================
FILE: PyPtt/_api_post.py
================================================
from __future__ import annotations

from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import log


def fast_post_step0(api, board: str, title: str, content: str, post_type: int) -> None:
    _api_util.goto_board(api, board)

    cmd_list = []
    cmd_list.append(command.ctrl_p)
    cmd_list.append(str(post_type))
    cmd_list.append(command.enter)
    cmd_list.append(str(title))
    cmd_list.append(command.enter)
    cmd_list.append(str(content))
    cmd_list.append(command.ctrl_x)
    cmd_list.append('s')
    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('發表文章於【', break_detect=True),
        connect_core.TargetUnit('使用者不可發言', break_detect=True),
        connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
        connect_core.TargetUnit('任意鍵繼續', break_detect=True),
        connect_core.TargetUnit('確定要儲存檔案嗎', break_detect=True)
    ]
    index = api.connect_core.fast_send(cmd, target_list)
    if index < 0:
        raise exceptions.UnknownError('UnknownError')
    if index == 1 or index == 2:
        raise exceptions.NoPermission(i18n.no_permission)


def fast_post_step1(api: object, sign_file) -> None:
    cmd = '\r'

    target_list = [
        connect_core.TargetUnit('發表文章於【', break_detect=True),
        connect_core.TargetUnit('使用者不可發言', break_detect=True),
        connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
        connect_core.TargetUnit('任意鍵繼續', break_detect=True),
        connect_core.TargetUnit('確定要儲存檔案嗎', break_detect=True),
        connect_core.TargetUnit('x=隨機', response=str(sign_file) + '\r'),
    ]
    index = api.connect_core.fast_send(cmd, target_list)
    if index < 0:
        raise exceptions.UnknownError('UnknownError')


def fast_post(
        api: object,
        board: str,
        title: str,
        content: str,
        post_type: int,
        sign_file) -> None:
    _api_util.goto_board(api, board)

    cmd_list = []
    cmd_list.append(command.ctrl_p)
    cmd_list.append(str(post_type))
    cmd_list.append(command.enter)
    cmd_list.append(str(title))
    cmd_list.append(command.enter)
    cmd_list.append(str(content))
    cmd_list.append(command.ctrl_x)
    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('發表文章於【', break_detect=True),
        connect_core.TargetUnit('使用者不可發言', break_detect=True),
        connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
        connect_core.TargetUnit('任意鍵繼續', break_detect=True),
        connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
        connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),
    ]
    index = api.connect_core.fast_send(cmd, target_list)
    if index < 0:
        raise exceptions.UnknownError('UnknownError')
    if index == 1 or index == 2:
        raise exceptions.NoPermission(i18n.no_permission)


sign_file_list = [str(x) for x in range(0, 10)]
sign_file_list.append('x')


def post(api, board: str, title: str, content: str, title_index: int, sign_file: [str | int]) -> None:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    check_value.check_type(board, str, 'board')
    check_value.check_type(title_index, int, 'title_index')
    check_value.check_type(title, str, 'title')
    check_value.check_type(content, str, 'content')

    if str(sign_file).lower() not in sign_file_list:
        raise ValueError(f'wrong parameter sign_file: {sign_file}')

    _api_util.check_board(api, board)
    _api_util.goto_board(api, board)

    log.logger.info(i18n.post)

    cmd_list = []
    cmd_list.append(command.ctrl_p)

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('發表文章於【', break_detect=True),
        connect_core.TargetUnit('使用者不可發言', break_detect=True),
        connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
    ]
    index = api.connect_core.send(cmd, target_list)
    if index < 0:
        raise exceptions.UnknownError('UnknownError')
    if index == 1 or index == 2:
        log.logger.info(i18n.post, '...', i18n.fail)
        raise exceptions.NoPermission(i18n.no_permission)

    log.logger.debug(i18n.has_post_permission)

    content = lib_util.uniform_new_line(content)

    cmd_list = []
    cmd_list.append(str(title_index))
    cmd_list.append(command.enter)
    cmd_list.append(str(title))
    cmd_list.append(command.enter)
    cmd_list.append(command.ctrl_y * 40)
    cmd_list.append(str(content))
    cmd_list.append(command.ctrl_x)
    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('任意鍵繼續', break_detect=True),
        connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
        connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),
    ]
    api.connect_core.send(
        cmd,
        target_list,
        screen_timeout=api.config.screen_post_timeout)

    log.logger.info(i18n.post, '...', i18n.success)


================================================
FILE: PyPtt/_api_reply_post.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log


def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: str, sign_file, post_aid: str,
               post_index: int) -> None:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not isinstance(reply_to, data_type.ReplyTo):
        raise TypeError(f'ReplyTo must be data_type.ReplyTo')

    check_value.check_type(board, str, 'board')
    check_value.check_type(content, str, 'content')
    if post_aid is not None:
        check_value.check_type(post_aid, str, 'PostAID')

    if post_index != 0:
        newest_index = api.get_newest_index(
            data_type.NewIndex.BOARD,
            board=board)
        check_value.check_index(
            'index',
            post_index,
            max_value=newest_index)

    sign_file_list = ['x']
    sign_file_list.extend([str(x) for x in range(0, 10)])

    if str(sign_file).lower() not in sign_file_list:
        raise ValueError(f'wrong parameter sign_file: {sign_file}')

    if post_aid is not None and post_index != 0:
        raise ValueError('wrong parameter aid and index can\'t both input')

    _api_util.check_board(api, board)

    _api_util.goto_board(api, board)

    cmd_list = []

    if post_aid is not None:
        cmd_list.append(lib_util.check_aid(post_aid))
    elif post_index != 0:
        cmd_list.append(str(post_index))
    else:
        raise ValueError('post_aid and post_index cannot be None at the same time')

    cmd_list.append(command.enter * 2)
    cmd_list.append('r')

    if reply_to == data_type.ReplyTo.BOARD:
        log.logger.info(i18n.reply_board)
        reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='F' + command.enter)
    elif reply_to == data_type.ReplyTo.MAIL:
        log.logger.info(i18n.reply_mail)
        reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='M' + command.enter)
    elif reply_to == data_type.ReplyTo.BOARD_MAIL:
        log.logger.info(i18n.reply_board_mail)
        reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='B' + command.enter)

    cmd = ''.join(cmd_list)
    target_list = [
        connect_core.TargetUnit('任意鍵繼續', break_detect=True),
        connect_core.TargetUnit('◆ 很抱歉, 此文章已結案並標記, 不得回應', log_level=log.INFO,
                                exceptions_=exceptions.CantResponse()),
        connect_core.TargetUnit('(E)繼續編輯 (W)強制寫入', log_level=log.INFO, response='W' + command.enter),
        connect_core.TargetUnit('請選擇簽名檔', response=str(sign_file) + command.enter),
        connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
        connect_core.TargetUnit('編輯文章', log_level=log.INFO,
                                response=str(content) + command.enter + command.ctrl_x),
        connect_core.TargetUnit('請問要引用原文嗎', log_level=log.DEBUG, response='Y' + command.enter),
        connect_core.TargetUnit('採用原標題[Y/n]?', log_level=log.DEBUG, response='Y' + command.enter),
        reply_target_unit,
        connect_core.TargetUnit('已順利寄出,是否自存底稿', log_level=log.DEBUG, response='Y' + command.enter),
    ]

    api.connect_core.send(
        cmd,
        target_list,
        screen_timeout=api.config.screen_long_timeout)

    log.logger.info(i18n.success)


================================================
FILE: PyPtt/_api_search_user.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import log


def search_user(api, ptt_id: str, min_page: int, max_page: int) -> list:
    _api_util.one_thread(api)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    check_value.check_type(ptt_id, str, 'ptt_id')
    if min_page is not None:
        check_value.check_index('min_page', min_page)
    if max_page is not None:
        check_value.check_index('max_page', max_page)
    if min_page is not None and max_page is not None:
        check_value.check_index_range('min_page', min_page, 'max_page', max_page)

    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append('T')
    cmd_list.append(command.enter)
    cmd_list.append('Q')
    cmd_list.append(command.enter)
    cmd_list.append(ptt_id)
    cmd = ''.join(cmd_list)

    if min_page is not None:
        template = min_page
    else:
        template = 1

    appendstr = ' ' * template
    cmdtemp = cmd + appendstr

    target_list = [
        connect_core.TargetUnit('任意鍵', break_detect=True)]

    resultlist = []

    log.logger.info(i18n.search_user)

    while True:

        api.connect_core.send(
            cmdtemp,
            target_list)
        ori_screen = api.connect_core.get_screen_queue()[-1]
        # print(OriScreen)
        # print(len(OriScreen.split('\n')))

        if len(ori_screen.split('\n')) == 2:
            result_id = ori_screen.split('\n')[1]
            result_id = result_id[result_id.find(' ') + 1:].strip()
            # print(result_id)

            resultlist.append(result_id)
            break
        else:

            ori_screen = ori_screen.split('\n')[3:-1]
            ori_screen = '\n'.join(ori_screen)

            templist = ori_screen.replace('\n', ' ')

            while '  ' in templist:
                templist = templist.replace('  ', ' ')

            templist = templist.split(' ')
            resultlist.extend(templist)

            # print(templist)
            # print(len(templist))

            if len(templist) != 100 and len(templist) != 120:
                break

            template += 1
            if max_page is not None:
                if template > max_page:
                    break

            cmdtemp = ' '

    api.connect_core.send(
        command.enter,
        [
            # 《ID暱稱》
            connect_core.TargetUnit('《ID暱稱》', response=command.enter),
            connect_core.TargetUnit('查詢網友', break_detect=True)
        ]
    )
    log.logger.info(i18n.success)

    return list(filter(None, resultlist))


================================================
FILE: PyPtt/_api_set_board_title.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util


def set_board_title(api, board: str, new_title: str) -> None:
    # 第一支板主專用 api
    _api_util.one_thread(api)

    _api_util.goto_board(api, board)

    if not api._is_login:
        raise exceptions.RequireLogin(i18n.require_login)

    if not api.is_registered_user:
        raise exceptions.UnregisteredUser(lib_util.get_current_func_name())

    check_value.check_type(board, str, 'board')
    check_value.check_type(new_title, str, 'new_title')

    _api_util.check_board(
        api,
        board,
        check_moderator=True)

    cmd_list = []
    cmd_list.append('I')
    cmd_list.append(command.ctrl_p)
    cmd_list.append('b')
    cmd_list.append(command.enter)
    cmd_list.append(command.backspace * 31)
    cmd_list.append(new_title)
    cmd_list.append(command.enter * 2)
    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('◆ 已儲存新設定', break_detect=True),
        connect_core.TargetUnit('◆ 未改變任何設定', break_detect=True),
    ]

    api.connect_core.send(
        cmd,
        target_list,
        screen_timeout=api.config.screen_long_timeout)


================================================
FILE: PyPtt/_api_util.py
================================================
from __future__ import annotations

import functools
import re
import threading
from typing import Dict, Optional

from . import _api_get_board_info
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import log
from . import screens


def get_content(api, post_mode: bool = True):
    api.Unconfirmed = False

    def is_unconfirmed_handler(screen):
        api.Unconfirmed = True

    if post_mode:
        cmd = command.enter * 2
    else:
        cmd = command.enter

    target_list = [
        # 待證實文章
        connect_core.TargetUnit('本篇文章內容經站方授權之板務管理人員判斷有尚待證實之處', response=' ',
                                handler=is_unconfirmed_handler),
        connect_core.TargetUnit(screens.Target.PostEnd, log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit(screens.Target.InPost, log_level=log.DEBUG, break_detect=True),
        connect_core.TargetUnit(screens.Target.PostNoContent, log_level=log.DEBUG, break_detect=True),
        # 動畫文章
        connect_core.TargetUnit(screens.Target.Animation, response=command.go_main_menu_type_q,
                                break_detect_after_send=True),
    ]

    line_from_pattern = re.compile('[\d]+~[\d]+')

    has_control_code = False
    control_code_mode = False
    push_start = False
    content_start_exist = False
    content_start_jump = False
    content_start_jump_set = False

    first_page = True
    origin_post = []
    stop_dict = dict()

    while True:
        index = api.connect_core.send(cmd, target_list)
        if index == 3 or index == 4:
            return None, False

        last_screen = api.connect_core.get_screen_queue()[-1]
        lines = last_screen.split('\n')
        last_line = lines[-1]
        lines.pop()
        last_screen = '\n'.join(lines)

        if screens.Target.content_start in last_screen and not content_start_exist:
            content_start_exist = True

        if content_start_exist:
            if not content_start_jump_set:
                if screens.Target.content_start not in last_screen:
                    content_start_jump = True
                    content_start_jump_set = True
            else:
                content_start_jump = False

        pattern_result = line_from_pattern.search(last_line)
        if pattern_result is None:
            control_code_mode = True
            has_control_code = True
        else:
            last_read_line_list = pattern_result.group(0).split('~')
            last_read_line_a_temp = int(last_read_line_list[0])
            last_read_line_b_temp = int(last_read_line_list[1])
            if control_code_mode:
                last_read_line_a = last_read_line_a_temp - 1
                last_read_line_b = last_read_line_b_temp - 1
            control_code_mode = False

        if first_page:
            first_page = False
            origin_post.append(last_screen)
        else:
            # 這裡是根據觀察畫面行數的變化歸納出的神奇公式...
            # 輸出的結果是要判斷出畫面的最後 x 行是新的文章內容
            #
            # 這裡是 PyPtt 最黑暗最墮落的地方,所有你所知的程式碼守則,在這裡都不適用
            # 每除完一次錯誤,我會陷入嚴重的創傷後壓力症候群,而我的腦袋會自動選擇遺忘這裡所有的一切
            # 以確保下一個週一,我可以正常上班
            # but it works!

            # print(LastScreen)
            # print(f'last_read_line_a_temp [{last_read_line_a_temp}]')
            # print(f'last_read_line_b_temp [{last_read_line_b_temp}]')
            # print(f'last_read_line_a {last_read_line_a}')
            # print(f'last_read_line_b {last_read_line_b}')
            # print(f'GetLineB {last_read_line_a_temp - last_read_line_a}')
            # print(f'GetLineA {last_read_line_b_temp - last_read_line_b}')
            # print(f'show line {last_read_line_b_temp - last_read_line_a_temp + 1}')
            if not control_code_mode:

                if last_read_line_a_temp in stop_dict:
                    new_content_part = '\n'.join(
                        lines[-stop_dict[last_read_line_a_temp]:])
                    stop_dict = dict()
                else:
                    get_line_b = last_read_line_b_temp - last_read_line_b
                    if get_line_b > 0:
                        # print('Type 1')
                        new_content_part = '\n'.join(lines[-get_line_b:])
                        if index == 1 and len(new_content_part) == get_line_b - 1:
                            new_content_part = '\n'.join(lines[-(get_line_b * 2):])
                        elif origin_post:
                            last_line_temp = origin_post[-1].strip()
                            try_line = lines[-(get_line_b + 1)].strip()

                            if not last_line_temp.endswith(try_line):
                                new_content_part = try_line + '\n' + new_content_part
                        stop_dict = dict()
                    else:
                        # 駐足現象,LastReadLineB跟上一次相比並沒有改變
                        if (last_read_line_b_temp + 1) not in stop_dict:
                            stop_dict[last_read_line_b_temp + 1] = 1
                        stop_dict[last_read_line_b_temp + 1] += 1

                        get_line_a = last_read_line_a_temp - last_read_line_a

                        if get_line_a > 0:
                            # print(f'Type 2 get_line_a [{get_line_a}]')
                            new_content_part = '\n'.join(lines[-get_line_a:])
                        else:
                            new_content_part = '\n'.join(lines)

            else:
                new_content_part = lines[-1]

            origin_post.append(new_content_part)

            log.logger.debug('NewContentPart', new_content_part)

        if index == 1:
            if content_start_jump and len(new_content_part) == 0:
                get_line_b += 1
                new_content_part = '\n'.join(lines[-get_line_b:])

                origin_post.pop()
                origin_post.append(new_content_part)
            break

        if not control_code_mode:
            last_read_line_a = last_read_line_a_temp
            last_read_line_b = last_read_line_b_temp

        for EC in screens.Target.content_end_list:
            if EC in last_screen:
                push_start = True
                break

        if push_start:
            cmd = command.right
        else:
            cmd = command.down

    # print(api.Unconfirmed)
    origin_post = '\n'.join(origin_post)
    # OriginPost = [line.strip() for line in OriginPost.split('\n')]
    # OriginPost = '\n'.join(OriginPost)

    log.logger.debug('OriginPost', origin_post)

    return origin_post, has_control_code


mail_capacity: Optional[tuple[int, int]] = None


def get_mailbox_capacity(api) -> tuple[int, int]:
    global mail_capacity
    if mail_capacity is not None:
        return mail_capacity

    last_screen = api.connect_core.get_screen_queue()[-1]
    capacity_line = last_screen.split('\n')[2]

    log.logger.debug('capacity_line', capacity_line)

    pattern_result = re.compile('(\d+)/(\d+)').search(capacity_line)
    if pattern_result is not None:
        current_capacity = int(pattern_result.group(0).split('/')[0])
        max_capacity = int(pattern_result.group(0).split('/')[1])

        log.logger.debug('current_capacity', current_capacity)
        log.logger.debug('max_capacity', max_capacity)

        mail_capacity = (current_capacity, max_capacity)
        return current_capacity, max_capacity
    return 0, 0


# >     1   112/09 ericsk       □ [心得] 終於開板了
# ┌── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─┐
# │ 文章代碼(AID): #13cPSYOX (Python) [ptt.cc] [心得] 終於開板了  │
# │ 文章網址: https://www.ptt.cc/bbs/Python/M.1134139170.A.621.html      │
# │ 這一篇文章值 2 Ptt幣                                              │
# └── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─┘

def parse_query_post(api, ori_screen):
    lock_post = False
    try:
        cursor_line = [line for line in ori_screen.split(
            '\n') if line.strip().startswith(api.cursor)][0]
    except Exception as e:
        print(api.cursor)
        print(ori_screen)
        raise e

    post_author = cursor_line
    if '□' in post_author:
        post_author = post_author[:post_author.find('□')].strip()
    elif 'R:' in post_author:
        post_author = post_author[:post_author.find('R:')].strip()
    elif ' 轉 ' in post_author:
        post_author = post_author[:post_author.find('轉')].strip()
    elif ' 鎖 ' in post_author:
        post_author = post_author[:post_author.find('鎖')].strip()
        lock_post = True
    post_author = post_author[post_author.rfind(' '):].strip()

    post_title = cursor_line
    if ' □ ' in post_title:
        post_title = post_title[post_title.find('□') + 1:].strip()
    elif ' R:' in post_title:
        post_title = post_title[post_title.find('R:'):].strip()
    elif ' 轉 ' in post_title:
        post_title = post_title[post_title.find('轉') + 1:].strip()
        post_title = f'Fw: {post_title}'
    elif ' 鎖 ' in post_title:
        post_title = post_title[post_title.find('鎖') + 1:].strip()

    ori_screen_temp = ori_screen[ori_screen.find('┌──'):]
    ori_screen_temp = ori_screen_temp[:ori_screen_temp.find('└──')]

    aid_line = [line for line in ori_screen.split(
        '\n') if line.startswith('│ 文章代碼(AID)')]

    post_aid = None
    if len(aid_line) == 1:
        aid_line = aid_line[0]
        pattern = re.compile('#[\w|-]+')
        pattern_result = pattern.search(aid_line)
        post_aid = pattern_result.group(0)[1:]

    pattern = re.compile('文章網址: https:[\S]+html')
    pattern_result = pattern.search(ori_screen_temp)

    if pattern_result is None:
        post_web = None
    else:
        post_web = pattern_result.group(0)[6:]

    pattern = re.compile('這一篇文章值 [\d]+ Ptt幣')
    pattern_result = pattern.search(ori_screen_temp)
    if pattern_result is None:
        # 特殊文章無價格
        post_money = -1
    else:
        post_money = pattern_result.group(0)[7:]
        post_money = post_money[:post_money.find(' ')]
        post_money = int(post_money)

    pattern = re.compile('[\d]+\/[\d]+')
    pattern_result = pattern.search(cursor_line)
    if pattern_result is None:
        list_date = None
    else:
        list_date = pattern_result.group(0)
        list_date = list_date[-5:]
    # print(list_date)

    # >  7485   9 8/09 CodingMan    □ [閒聊] PTT Library 更新
    # > 79189 M 1 9/17 LittleCalf   □ [公告] 禁言退文公告
    # >781508 +爆 9/17 jodojeda     □ [新聞] 國人吃魚少 學者:應把吃魚當成輕鬆愉快
    # >781406 +X1 9/17 kingofage111 R: [申請] ReDmango 請辭Gossiping板主職務

    pattern = re.compile('[\d]+')
    pattern_result = pattern.search(cursor_line)
    post_index = 0
    if pattern_result is not None:
        post_index = int(pattern_result.group(0))

    push_number = cursor_line
    push_number = push_number[7:11]
    push_number = push_number.split(' ')
    push_number = list(filter(None, push_number))

    if len(push_number) == 0:
        push_number = None
    else:
        push_number = push_number[-1]
        if push_number.startswith('爆') or push_number.startswith('~爆'):
            push_number = '爆'

        if push_number.startswith('+') or push_number.startswith('~'):
            push_number = push_number[1:]

        if push_number.lower().startswith('m'):
            push_number = push_number[1:]

        if push_number.lower().startswith('!'):
            push_number = push_number[1:]

        if push_number.lower().startswith('s'):
            push_number = push_number[1:]

        if push_number.lower().startswith('='):
            push_number = push_number[1:]

        if len(push_number) == 0:
            push_number = None

    log.logger.debug('PostAuthor', post_author)
    log.logger.debug('PostTitle', post_title)
    log.logger.debug('PostAID', post_aid)
    log.logger.debug('PostWeb', post_web)
    log.logger.debug('PostMoney', post_money)
    log.logger.debug('ListDate', list_date)
    log.logger.debug('PushNumber', push_number)

    return lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index


def get_search_condition_cmd(index_type: data_type.NewIndex, search_list: Optional[list] = None):
    cmd_list = []
    if not search_list:
        return cmd_list

    for search_type, search_condition in search_list:

        if search_type == data_type.SearchType.KEYWORD:
            cmd_list.append('/')
        elif search_type == data_type.SearchType.AUTHOR:
            cmd_list.append('a')
        elif search_type == data_type.SearchType.MARK:
            cmd_list.append('G')
        elif index_type == data_type.NewIndex.BOARD:
            if search_type == data_type.SearchType.COMMENT:
                cmd_list.append('Z')
            elif search_type == data_type.SearchType.MONEY:
                cmd_list.append('A')
            else:
                continue
        else:
            continue

        cmd_list.append(search_condition)
        cmd_list.append(command.enter)

    return cmd_list


def goto_board(api, board: str, refresh: bool = False, end: bool = False) -> None:
    cmd_list = []
    cmd_list.append(command.go_main_menu)
    cmd_list.append('qs')
    cmd_list.append(board)
    cmd_list.append(command.enter)
    cmd_list.append(command.space)

    cmd = ''.join(cmd_list)

    target_list = [
        connect_core.TargetUnit('任意鍵', log_level=log.DEBUG, response=' '),
        connect_core.TargetUnit('互動式動畫播放中', log_level=log.DEBUG, response=command.ctrl_c),
        connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
    ]

    if refresh:
        current_refresh = True
    else:
        if board.lower() in api._goto_board_list:
            current_refresh = True
        else:
            current_refresh = False
    api._goto_board_list.append(board.lower())
    api.connect_core.send(cmd, target_list, refresh=current_refresh)

    if end:
        cmd_list = []
        cmd_list.append('1')
        cmd_list.append(command.enter)
        cmd_list.append('$')
        cmd = ''.join(cmd_list)

        target_list = [
            connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
        ]

        api.connect_core.send(cmd, target_list)


def one_thread(api):
    current_thread_id = threading.get_ident()
    if current_thread_id != api._thread_id:
        raise exceptions.MultiThreadOperated()


@functools.lru_cache(maxsize=64)
def check_board(api, board: str, check_moderator: bool = False) -> Dict:
    if board.lower() not in api._exist_board_list:
        board_info = _api_get_board_info.get_board_info(api, board, get_post_kind=False, call_by_others=False)
        api._exist_board_list.append(board.lower())
        api._board_info_list[board.lower()] = board_info

        moderators = board_info[data_type.BoardField.moderators]
        moderators = [x.lower() for x in moderators]
        api._moderators[board.lower()] = moderators
        api._board_info_list[board.lower()] = board_info

    if check_moderator:
        if api.ptt_id.lower() not in api._moderators[board.lower()]:
            raise exceptions.NeedModeratorPermission(board)

    return api._board_info_list[board.lower()]


================================================
FILE: PyPtt/check_value.py
================================================
from . import i18n
from . import log


def check_type(value, value_type, name) -> None:
    if not isinstance(value, value_type):
        if value_type is str:
            raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_string}, but got {value}')
        elif value_type is int:
            raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_integer}, but got {value}')
        elif value_type is bool:
            raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_boolean}, but got {value}')
        else:
            raise TypeError(f'[PyPtt] {name} {i18n.must_be} {value_type}, but got {value}')


def check_range(value, min_value, max_value, name) -> None:
    check_type(value, int, name)
    check_type(min_value, int, 'min_value')
    check_type(max_value, int, 'max_value')

    if min_value <= value <= max_value:
        return
    raise ValueError(f'{name} {value} {i18n.must_between} {min_value} ~ {max_value}')


def check_index(name, index, max_value=None) -> None:
    check_type(index, int, name)
    if index < 1:
        raise ValueError(f'{name} {i18n.must_bigger_than} 0')

    if max_value is not None:
        if index > max_value:
            log.logger.info('index', index)
            log.logger.info('max_value', max_value)
            raise ValueError(f'{name} {index} {i18n.must_between} 0 ~ {max_value}')


def check_index_range(start_name, start_index, end_name, end_index, max_value=None) -> None:
    check_type(start_index, int, start_name)
    check_type(end_index, int, end_name)

    if start_index < 1:
        raise ValueError(f'{start_name} {start_index} {i18n.must_bigger_than} 0')

    if end_index <= 1:
        raise ValueError(f'{end_name} {end_index} {i18n.must_bigger_than} 1')

    if start_index > end_index:
        raise ValueError(f'{end_name} {end_index} {i18n.must_bigger_than} {start_name} {start_index}')

    if max_value is not None:
        if start_index > max_value:
            raise ValueError(f'{start_name} {start_index} {i18n.must_small_than} {max_value}')

        if end_index > max_value:
            raise ValueError(f'{end_name} {end_index} {i18n.must_small_than} {max_value}')


if __name__ == '__main__':
    QQ = str

    if QQ is str:
        print('1')

    if QQ == str:
        print('2')

    if isinstance('', QQ):
        print('3')


================================================
FILE: PyPtt/command.py
================================================
# http://www.physics.udel.edu/~watson/scen103/ascii.html
enter = '\r'
tab = '\t'
ctrl_c = '\x03'
ctrl_d = '\x04'
ctrl_e = '\x05'
ctrl_f = '\x06'
ctrl_h = '\x08'
ctrl_l = '\x0C'
ctrl_p = '\x10'
ctrl_u = '\x15'
ctrl_x = '\x18'
ctrl_y = '\x19'
ctrl_z = '\x1A'
star = '\x2A'
up = '\x1b\x4fA'
down = '\x1b\x4fB'
right = '\x1b\x4fC'
left = '\x1b\x4fD'

space = ' '
query_post = 'Q'
comment = 'X'
go_main_menu = ' ' + left * 5
go_main_menu_type_q = 'q' * 5
refresh = ctrl_l
control_code = ctrl_u + star
backspace = ctrl_h


================================================
FILE: PyPtt/config.py
================================================
from . import data_type
from . import log


class Config:
    # retry_wait_time 秒後重新連線
    retry_wait_time = 3

    # ScreenLTimeOut 秒後判定此畫面沒有可辨識的目標
    screen_timeout = 3.0

    # screen_long_timeout 秒後判定此畫面沒有可辨識的目標
    # 適用於需要特別等待的情況,例如: 剔除其他登入等等
    # 建議不要低於 10 秒,剔除其他登入最長可能會花費約六到七秒
    screen_long_timeout = 10.0

    # screen_post_timeout 秒後判定此畫面沒有可辨識的目標
    # 適用於貼文等待的情況,建議不要低於 60 秒
    screen_post_timeout = 60.0

    # 預設語言
    language = data_type.Language.MANDARIN

    # 預設 log 等級
    log_level = log.INFO

    # 預設不剔除其他登入
    kick_other_session = False

    # 預設登入 PTT1
    host = data_type.HOST.PTT1

    # 預設採用 websockets
    connect_mode = None

    # 預設使用 23
    port = 23

    logger_callback = None


LOGGER_CONFIG = {

}


================================================
FILE: PyPtt/connect_core.py
================================================
from __future__ import annotations

import asyncio
import ssl
import telnetlib
import threading
import time
import traceback
import warnings
from typing import Any

import websockets
import websockets.exceptions
import websockets.http

import PyPtt
from . import command
from . import data_type
from . import exceptions
from . import i18n
from . import log
from . import screens

websockets.http.USER_AGENT += f' PyPtt/{PyPtt.__version__}'

ssl_context = ssl.create_default_context()


class TargetUnit:
    def __init__(self, detect_target, log_level: log.LogLevel = None, response: [Any | str] = '', break_detect=False,
                 break_detect_after_send=False, exceptions_=None, refresh=True, secret=False, handler=None,
                 max_match: int = 0):

        self.detect_target = detect_target
        if log_level is None:
            self.log_level = log.INFO
        else:
            self.log_level = log_level
        self._response_func = response
        self._break_detect = break_detect
        self._exception = exceptions_
        self._refresh = refresh
        self._break_after_send = break_detect_after_send
        self._secret = secret
        self._Handler = handler
        self._max_match = max_match
        self._current_match = 0

    def is_match(self, screen: str) -> bool:
        if self._current_match >= self._max_match > 0:
            return False
        if isinstance(self.detect_target, str):
            if self.detect_target in screen:
                self._current_match += 1
                return True
            return False
        elif isinstance(self.detect_target, list):
            for Target in self.detect_target:
                if Target not in screen:
                    return False
            self._current_match += 1
            return True

    def get_detect_target(self):
        return self.detect_target

    def get_log_level(self):
        return self.log_level

    def get_response(self, screen: str) -> str:
        if callable(self._response_func):
            return self._response_func(screen)
        return self._response_func

    def is_break(self) -> bool:
        return self._break_detect

    def raise_exception(self):
        if isinstance(self._exception, Exception):
            raise self._exception

    def is_refresh(self) -> bool:
        return self._refresh

    def is_break_after_send(self) -> bool:
        return self._break_after_send

    def is_secret(self) -> bool:
        return self._secret


class RecvData:
    def __init__(self):
        self.data = None


async def websocket_recv_func(core, recv_data_obj):
    recv_data_obj.data = await core.recv()


async def websocket_receiver(core, screen_timeout, recv_data_obj):
    # Wait for at most 1 second
    await asyncio.wait_for(
        websocket_recv_func(core, recv_data_obj),
        timeout=screen_timeout)


class ReceiveDataQueue(object):
    def __init__(self):
        self._ReceiveDataQueue = []

    def add(self, screen):
        self._ReceiveDataQueue.append(screen)
        self._ReceiveDataQueue = self._ReceiveDataQueue[-10:]

    def get(self, last=1):
        return self._ReceiveDataQueue[-last:]


class API(object):
    def __init__(self, config):

        self.current_encoding = 'big5uao'
        self.config = config
        self._RDQ = ReceiveDataQueue()
        self._UseTooManyResources = TargetUnit(screens.Target.use_too_many_resources,
                                               exceptions_=exceptions.UseTooManyResources())

    def connect(self) -> None:
        def _wait():
            for i in range(self.config.retry_wait_time):

                if self.config.host == data_type.HOST.PTT1:
                    log.logger.info(i18n.prepare_connect_again, i18n.PTT, str(self.config.retry_wait_time - i))
                elif self.config.host == data_type.HOST.PTT2:
                    log.logger.info(i18n.prepare_connect_again, i18n.PTT2, str(self.config.retry_wait_time - i))
                elif self.config.host == data_type.HOST.LOCALHOST:
                    log.logger.info(i18n.prepare_connect_again, i18n.localhost, str(self.config.retry_wait_time - i))
                else:
                    log.logger.info(i18n.prepare_connect_again, self.config.host, str(self.config.retry_wait_time - i))

                time.sleep(1)

        warnings.filterwarnings("ignore", category=DeprecationWarning)

        self.current_encoding = 'big5uao'
        # self.log.py.info(i18n.connect_core, i18n.active)

        if self.config.host == data_type.HOST.PTT1:
            telnet_host = 'ptt.cc'
            websocket_host = 'wss://ws.ptt.cc/bbs/'
            websocket_origin = 'https://term.ptt.cc'
        elif self.config.host == data_type.HOST.PTT2:
            telnet_host = 'ptt2.cc'
            websocket_host = 'wss://ws.ptt2.cc/bbs/'
            websocket_origin = 'https://term.ptt2.cc'
        elif self.config.host == data_type.HOST.LOCALHOST:
            telnet_host = 'localhost'
            websocket_host = 'wss://localhost'
            websocket_origin = 'https://term.ptt.cc'
        else:
            telnet_host = self.config.host
            websocket_host = f'wss://{self.config.host}'
            websocket_origin = 'https://term.ptt.cc'

        connect_success = False

        for _ in range(2):

            try:
                if self.config.connect_mode == data_type.ConnectMode.TELNET:
                    self._core = telnetlib.Telnet(telnet_host, self.config.port)
                else:
                    if not threading.current_thread() is threading.main_thread():
                        loop = asyncio.new_event_loop()
                        asyncio.set_event_loop(loop)

                    log.logger.debug('USER_AGENT', websockets.http.USER_AGENT)
                    self._core = asyncio.get_event_loop().run_until_complete(
                        websockets.connect(
                            websocket_host,
                            origin=websocket_origin,
                            ssl=ssl_context))

                connect_success = True
            except Exception as e:
                traceback.print_tb(e.__traceback__)
                print(e)

                if self.config.host == data_type.HOST.PTT1:
                    log.logger.info(i18n.connect, i18n.PTT, i18n.fail)
                elif self.config.host == data_type.HOST.PTT2:
                    log.logger.info(i18n.connect, i18n.PTT2, i18n.fail)
                elif self.config.host == data_type.HOST.LOCALHOST:
                    log.logger.info(i18n.connect, i18n.localhost, i18n.fail)
                else:
                    log.logger.info(i18n.connect, self.config.host, i18n.fail)

                _wait()
                continue

            break

        if not connect_success:
            raise exceptions.ConnectError(self.config)

    def _decode_screen(self, receive_data_buffer, start_time, target_list, is_secret, refresh, msg):

        break_detect_after_send = False
        use_too_many_res = False

        vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding)
        screen = vt100_p.screen

        find_target = False
        target_index = -1
        for target in target_list:
            condition = target.is_match(screen)
            if condition:
                if target._Handler is not None:
                    target._Handler(screen)
                if len(screen) > 0:
                    screens.show(self.config, screen)
                    self._RDQ.add(screen)
                    if target == self._UseTooManyResources:
                        use_too_many_res = True
                        # print(f'1 {use_too_many_res}')
                        break
                    target.raise_exception()

                find_target = True

                end_time = time.time()
                log.logger.debug(i18n.spend_time, round(end_time - start_time, 3))

                if target.is_break():
                    target_index = target_list.index(target)
                    break

                msg = target.get_response(screen)

                add_refresh = False
                if target.is_refresh():
                    add_refresh = True
                elif refresh:
                    add_refresh = True

                if add_refresh:
                    if not msg.endswith(command.refresh):
                        msg = msg + command.refresh

                is_secret = target.is_secret()

                if target.is_break_after_send():
                    # break_index = target_list.index(target)
                    break_detect_after_send = True
                break
        return screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index

    def send(self, msg: str, target_list: list, screen_timeout: int = 0, refresh: bool = True,
             secret: bool = False) -> int:

        if not all(isinstance(T, TargetUnit) for T in target_list):
            raise ValueError('Item of TargetList must be TargetUnit')

        if self._UseTooManyResources not in target_list:
            target_list.append(self._UseTooManyResources)

        if screen_timeout == 0:
            current_screen_timeout = self.config.screen_timeout
        else:
            current_screen_timeout = screen_timeout

        break_detect_after_send = False
        is_secret = secret

        use_too_many_res = False
        while True:

            if refresh and not msg.endswith(command.refresh):
                msg = msg + command.refresh

            try:
                msg = msg.encode('utf-8', 'replace')
            except AttributeError:
                pass
            except Exception as e:
                traceback.print_tb(e.__traceback__)
                print(e)
                msg = msg.encode('utf-8', 'replace')

            if is_secret:
                log.logger.debug(i18n.send_msg, i18n.hide_sensitive_info)
            else:
                log.logger.debug(i18n.send_msg, str(msg))

            if self.config.connect_mode == data_type.ConnectMode.TELNET:
                try:
                    self._core.read_very_eager()
                    self._core.write(msg)
                except EOFError:
                    raise exceptions.ConnectionClosed()
            else:
                try:
                    asyncio.get_event_loop().run_until_complete(
                        self._core.send(msg))
                except websockets.exceptions.ConnectionClosedError:
                    raise exceptions.ConnectionClosed()
                except RuntimeError:
                    raise exceptions.ConnectionClosed()
                except websockets.exceptions.ConnectionClosedOK:
                    raise exceptions.ConnectionClosed()

                if break_detect_after_send:
                    return -1

            msg = ''
            receive_data_buffer = bytes()

            start_time = time.time()
            mid_time = time.time()
            while mid_time - start_time < current_screen_timeout:

                # print(1)
                recv_data_obj = RecvData()

                if self.config.connect_mode == data_type.ConnectMode.TELNET:
                    try:
                        recv_data_obj.data = self._core.read_very_eager()
                    except EOFError:
                        return -1

                else:
                    try:

                        asyncio.get_event_loop().run_until_complete(
                            websocket_receiver(
                                self._core, current_screen_timeout, recv_data_obj))

                    except websockets.exceptions.ConnectionClosed:
                        if use_too_many_res:
                            raise exceptions.UseTooManyResources()
                        raise exceptions.ConnectionClosed()
                    except websockets.exceptions.ConnectionClosedOK:
                        raise exceptions.ConnectionClosed()
                    except asyncio.TimeoutError:
                        return -1
                    except RuntimeError:
                        raise exceptions.ConnectionClosed()

                receive_data_buffer += recv_data_obj.data

                screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
                    self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)

                if self.current_encoding == 'big5uao' and not find_target:
                    self.current_encoding = 'utf-8'
                    screen_, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
                        self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)

                    if find_target:
                        screen = screen_
                    else:
                        self.current_encoding = 'big5uao'

                # print(4)
                if target_index != -1:
                    return target_index

                if use_too_many_res:
                    continue

                if find_target:
                    break
                if len(screen) > 0:
                    screens.show(self.config, screen)
                    self._RDQ.add(screen)

                # print(6)

                mid_time = time.time()

            if not find_target:
                return -1
        return -2

    def close(self):
        if self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:
            asyncio.get_event_loop().run_until_complete(self._core.close())
        else:
            self._core.close()

    def get_screen_queue(self) -> list:
        return self._RDQ.get(1)


================================================
FILE: PyPtt/data_type.py
================================================
import time
from enum import auto

from AutoStrEnum import AutoStrEnum


class Language:
    MANDARIN = 'zh_TW'
    ENGLISH = 'en_US'


class ConnectMode(AutoStrEnum):
    TELNET = auto()
    WEBSOCKETS = auto()


class SearchType(AutoStrEnum):
    """文章搜尋類型"""

    NOPE = auto()
    # 搜尋關鍵字    / ?
    KEYWORD = auto()
    # 搜尋作者      a
    AUTHOR = auto()
    # 搜尋推文數    Z
    COMMENT = auto()
    # 搜尋標記      G
    MARK = auto()
    # 搜尋稿酬      A
    MONEY = auto()


class ReplyTo(AutoStrEnum):
    # 回文類型

    BOARD = auto()
    MAIL = auto()
    BOARD_MAIL = auto()


class CommentType(AutoStrEnum):
    PUSH = auto()
    BOO = auto()
    ARROW = auto()


class UserField(AutoStrEnum):
    ptt_id = auto()
    money = auto()
    login_count = auto()
    account_verified = auto()
    legal_post = auto()
    illegal_post = auto()
    activity = auto()
    mail = auto()
    last_login_date = auto()
    last_login_ip = auto()
    five_chess = auto()
    chess = auto()
    signature_file = auto()


class CommentField(AutoStrEnum):
    type = auto()
    author = auto()
    content = auto()
    ip = auto()
    time = auto()


class PostStatus(AutoStrEnum):
    EXISTS = auto()
    DELETED_BY_AUTHOR = auto()
    DELETED_BY_MODERATOR = auto()
    DELETED_BY_UNKNOWN = auto()


class PostField(AutoStrEnum):
    board = auto()
    aid = auto()
    index = auto()
    author = auto()
    date = auto()
    title = auto()
    content = auto()
    money = auto()
    url = auto()
    ip = auto()
    comments = auto()
    post_status = auto()
    list_date = auto()
    has_control_code = auto()
    pass_format_check = auto()
    location = auto()
    push_number = auto()
    is_lock = auto()
    full_content = auto()
    is_unconfirmed = auto()


# class WaterballInfo:
#     def __init__(self, waterball_type, target, content, date):
#         self.type: int = parse_para(int, waterball_type)
#         self.target: str = parse_para(str, target)
#         self.content: str = parse_para(str, content)
#         self.date: str = parse_para(str, date)


class Cursor:
    # 舊式游標
    OLD: str = '●'
    # 新式游標
    NEW: str = '>'


class NewIndex(AutoStrEnum):
    # 看板
    BOARD = auto()
    # 信箱
    MAIL = auto()
    # 網頁,尚不支援
    # WEB = auto()


class HOST(AutoStrEnum):
    # 批踢踢萬
    PTT1 = auto()
    # 批踢踢兔
    PTT2 = auto()
    # 本機測試用
    LOCALHOST = auto()


class MarkType(AutoStrEnum):
    # s 文章
    S = auto()
    # 標記文章
    D = auto()
    # 刪除標記文章
    DELETE_D = auto()
    # M 起來
    M = auto()
    # 待證實文章
    UNCONFIRMED = auto()


class FavouriteBoardField(AutoStrEnum):
    board = auto()
    type = auto()
    title = auto()


class MailField(AutoStrEnum):
    origin_mail = auto()
    author = auto()
    title = auto()
    date = auto()
    content = auto()
    ip = auto()
    location = auto()
    is_red_envelope = auto()


class BoardField(AutoStrEnum):
    board = auto()
    online_user = auto()
    mandarin_des = auto()
    moderators = auto()
    open_status = auto()
    into_top_ten_when_hide = auto()
    can_non_board_members_post = auto()
    can_reply_post = auto()
    self_del_post = auto()
    can_comment_post = auto()
    can_boo_post = auto()
    can_fast_push = auto()
    min_interval_between_comments = auto()
    is_comment_record_ip = auto()
    is_comment_aligned = auto()
    can_moderators_del_illegal_content = auto()
    does_tran_post_auto_recorded_and_require_post_permissions = auto()
    is_cool_mode = auto()
    is_require18 = auto()
    require_login_time = auto()
    require_illegal_post = auto()
    # post_kind = auto()
    post_kind_list = auto()


class Compare(AutoStrEnum):
    BIGGER = auto()
    SAME = auto()
    SMALLER = auto()
    UNKNOWN = auto()


class TimedDict:
    def __init__(self, timeout: int = 0):
        self.timeout = timeout
        self.data = {}
        self.timestamps = {}

    def __setitem__(self, key, value):
        self.data[key] = value
        self.timestamps[key] = time.time()

    def __getitem__(self, key):
        if key not in self.data:
            raise KeyError(key)
        timestamp = self.timestamps[key]
        if time.time() - timestamp > self.timeout > 0:
            del self.data[key]
            del self.timestamps[key]
            raise KeyError(key)
        return self.data[key]

    def __contains__(self, key):
        try:
            self[key]
        except KeyError:
            return False
        else:
            return True

    def __len__(self):
        self.cleanup()
        return len(self.data)

    def cleanup(self):
        if self.timeout == 0:
            return

        now = time.time()
        to_remove = [key for key, timestamp in self.timestamps.items()
                     if now - timestamp > self.timeout > 0]
        for key in to_remove:
            del self.data[key]
            del self.timestamps[key]


================================================
FILE: PyPtt/exceptions.py
================================================
from . import data_type
from . import i18n


class Error(Exception):
    pass


class UnknownError(Error):
    def __init__(self, message):
        self.message = message

    def __str__(self):
        return self.message


class RequireLogin(Error):
    def __init__(self, message):
        self.message = message

    def __str__(self):
        return self.message


class NoPermission(Error):
    def __init__(self, message):
        self.message = message

    def __str__(self):
        return self.message


class LoginError(Error):
    def __init__(self):
        self.message = i18n.login_fail

    def __str__(self):
        return self.message


class NoFastComment(Error):
    def __init__(self):
        self.message = i18n.no_fast_comment

    def __str__(self):
        return self.message


class NoSuchUser(Error):
    def __init__(self, user):
        self.message = i18n.no_such_user + ': ' + user

    def __str__(self):
        return self.message


class NoSuchMail(Error):
    def __init__(self):
        self.message = i18n.no_such_mail

    def __str__(self):
        return self.message


# class UserOffline(Error):
#     def __init__(self, user):
#         self.message = i18n.user_offline + ': ' + user
#
#     def __str__(self):
#         return self.message


# class ParseError(Error):
#     def __init__(self, screen):
#         self.message = screen
#
#     def __str__(self):
#         return self.message


class NoMoney(Error):
    def __init__(self):
        self.message = i18n.no_money

    def __str__(self):
        return self.message


class NoSuchBoard(Error):
    def __init__(self, config, board):
        if config.host == data_type.HOST.PTT1:
            self.message = [
                i18n.PTT,
                i18n.no_such_board
            ]
        else:
            self.message = [
                i18n.PTT2,
                i18n.no_such_board
            ]

        if config.language == data_type.Language.MANDARIN:
            self.message = ''.join(self.message) + ': ' + board
        else:
            self.message = ' '.join(self.message) + ': ' + board

    def __str__(self):
        return self.message


class ConnectionClosed(Error):
    def __init__(self):
        self.message = i18n.connection_closed

    def __str__(self):
        return self.message


class UnregisteredUser(Error):
    def __init__(self, api_name):
        self.message = i18n.unregistered_user_cant_use_this_api + ': ' + api_name

    def __str__(self):
        return self.message


class MultiThreadOperated(Error):
    def __init__(self):
        self.message = i18n.multi_thread_operate

    def __str__(self):
        return self.message


class WrongIDorPassword(Error):
    def __init__(self):
        self.message = i18n.wrong_id_pw

    def __str__(self):
        return self.message


class WrongPassword(Error):
    def __init__(self):
        self.message = i18n.error_pw

    def __str__(self):
        return self.message


class LoginTooOften(Error):
    def __init__(self):
        self.message = i18n.login_too_often

    def __str__(self):
        return self.message


class UseTooManyResources(Error):
    def __init__(self):
        self.message = i18n.use_too_many_resources

    def __str__(self):
        return self.message


class HostNotSupport(Error):
    def __init__(self, api):
        self.message = f'{i18n.ptt2_not_support}: {api}'

    def __str__(self):
        return self.message


class CantComment(Error):
    def __init__(self):
        self.message = i18n.no_comment

    def __str__(self):
        return self.message


class CantResponse(Error):
    def __init__(self):
        self.message = i18n.no_response

    def __str__(self):
        return self.message


class NeedModeratorPermission(Error):
    def __init__(self, board):
        self.message = f'{i18n.need_moderator_permission}: {board}'

    def __str__(self):
        return self.message


class ConnectError(Error):
    def __init__(self, config):
        self.message = i18n.connect_fail

    def __str__(self):
        return self.message


class NoSuchPost(Error):
    def __init__(self, board, aid):
        self.message = i18n.replace(
            i18n.no_such_post,
            board,
            aid)

    def __str__(self):
        return self.message


class CanNotUseSearchPostCode(Error):
    """
    此狀態下無法使用搜尋文章代碼(AID)功能
    """

    def __init__(self):
        self.message = i18n.can_not_use_search_post_code_f

    def __str__(self):
        return self.message


class UserHasPreviouslyBeenBanned(Error):
    def __init__(self):
        self.message = i18n.user_has_previously_been_banned

    def __str__(self):
        return self.message


class MailboxFull(Error):
    def __init__(self):
        self.message = i18n.mail_box_full

    def __str__(self):
        return self.message


class NoSearchResult(Error):
    def __init__(self):
        self.message = i18n.no_search_result

    def __str__(self):
        return self.message


# 此帳號已設定為只能使用安全連線

class OnlySecureConnection(Error):
    def __init__(self):
        self.message = i18n.only_secure_connection

    def __str__(self):
        return self.message


class SetContactMailFirst(Error):
    def __init__(self):
        self.message = i18n.set_contact_mail_first

    def __str__(self):
        return self.message


class ResetYourContactEmail(Error):
    def __init__(self):
        self.message = i18n.reset_your_contact_email

    def __str__(self):
        return self.message


================================================
FILE: PyPtt/i18n.py
================================================
import os
import random

import yaml

from . import __version__
from . import data_type

locale_pool = {
    data_type.Language.ENGLISH,
    data_type.Language.MANDARIN
}

_script_path = os.path.dirname(os.path.abspath(__file__))
_lang_data = {}

mapping = {
    '{version}': __version__,
}


def replace(string, *args):
    for i in range(len(args)):
        target = f'{args[i]}'
        string = string.replace(f'_target{i}_', target)
    return string


def init(locale: str, cache: bool = False) -> None:
    if locale not in locale_pool:
        raise ValueError(f'Unknown locale: {locale}')

    language_file = f'{_script_path}/lang/{locale}.yaml'
    if not os.path.exists(language_file):
        raise ValueError(f'Unknown locale file: {language_file}')

    with open(language_file, "r") as f:
        string_data = yaml.safe_load(f)

    for k, v in string_data.items():

        if isinstance(v, list):
            v = random.choice(v)
        elif isinstance(v, str):
            pass
        else:
            raise ValueError(f'Unknown string data type: {v}')

        if locale == data_type.Language.ENGLISH:
            v = v[0].upper() + v[1:]

        for mk, mv in mapping.items():
            v = v.replace(mk, mv)

        globals()[k] = v
        if cache:
            global _lang_data
            _lang_data[k] = v


================================================
FILE: PyPtt/lang/en_US.yaml
================================================
PTT: PTT
PTT2: PTT2
active: Active
author: Author
board: Board
can_not_use_search_post_code_f: This status can not use the search PostField code function
catch_bottom_post_success: Catch bottom post success
change_pw: Change password
comment: Comment
comment_content: Comment content
comment_date: Comment date
comment_id: Comment id
connect: Connect
connect_core: Connect core
connect_fail: Connect fail
connect_mode_TELNET: Telnet
connect_mode_WEBSOCKET: WebSocket
connection_closed: Connection Closed
content: Content
current_version: Current version
date: Date
delete_post: Delete post
development_version: Running development version
done: Done
english_module: English
error_pw: Wrong password
fail: Fail
find_newest_index: Find newest index
get_board_info: Get board info _target0_
get_favourite_board_list: Query favourite board list
get_mail: Get mail
give_money_to: give _target0_ _target1_ P coins
goodbye:
  - good bye
  - bye
  - see you
  - catch you later
  - I hate to run, but...
  - Until we meet again, I will wait
has_comment_permission: User has permission to comment
has_new_mail_goto_main_menu: New mail! Back to main menu
has_post_permission: Have permission to post
hide_sensitive_info: Hide sensitive info
in_login_process_please_wait: In login process, please wait
initialization: Init
kick_other_login: Kick other login
latest_version: Running the latest version
localhost: Localhost
Download .txt
gitextract_7t96yf9r/

├── .github/
│   └── workflows/
│       ├── deploy.yml
│       ├── docs.yml
│       └── test.yml
├── .gitignore
├── GourceScript.bat
├── LICENSE
├── MANIFEST.in
├── PyPtt/
│   ├── PTT.py
│   ├── __init__.py
│   ├── _api_bucket.py
│   ├── _api_call_status.py
│   ├── _api_change_pw.py
│   ├── _api_comment.py
│   ├── _api_del_post.py
│   ├── _api_get_board_info.py
│   ├── _api_get_board_list.py
│   ├── _api_get_bottom_post_list.py
│   ├── _api_get_favourite_board.py
│   ├── _api_get_newest_index.py
│   ├── _api_get_post.py
│   ├── _api_get_post_index.py
│   ├── _api_get_time.py
│   ├── _api_get_user.py
│   ├── _api_give_money.py
│   ├── _api_has_new_mail.py
│   ├── _api_loginout.py
│   ├── _api_mail.py
│   ├── _api_mark_post.py
│   ├── _api_post.py
│   ├── _api_reply_post.py
│   ├── _api_search_user.py
│   ├── _api_set_board_title.py
│   ├── _api_util.py
│   ├── check_value.py
│   ├── command.py
│   ├── config.py
│   ├── connect_core.py
│   ├── data_type.py
│   ├── exceptions.py
│   ├── i18n.py
│   ├── lang/
│   │   ├── en_US.yaml
│   │   └── zh_TW.yaml
│   ├── lib_util.py
│   ├── log.py
│   ├── screens.py
│   └── service.py
├── README.md
├── docs/
│   ├── CNAME
│   ├── Makefile
│   ├── api/
│   │   ├── bucket.rst
│   │   ├── change_pw.rst
│   │   ├── comment.rst
│   │   ├── del_mail.rst
│   │   ├── del_post.rst
│   │   ├── get_aid_from_url.rst
│   │   ├── get_all_boards.rst
│   │   ├── get_board_info.rst
│   │   ├── get_bottom_post_list.rst
│   │   ├── get_favourite_boards.rst
│   │   ├── get_mail.rst
│   │   ├── get_newest_index.rst
│   │   ├── get_post.rst
│   │   ├── get_time.rst
│   │   ├── get_user.rst
│   │   ├── give_money.rst
│   │   ├── index.rst
│   │   ├── init.rst
│   │   ├── login_logout.rst
│   │   ├── mail.rst
│   │   ├── mark_post.rst
│   │   ├── post.rst
│   │   ├── reply_post.rst
│   │   ├── search_user.rst
│   │   └── set_board_title.rst
│   ├── changelog.rst
│   ├── conf.py
│   ├── dev.rst
│   ├── docker.rst
│   ├── examples.rst
│   ├── exceptions.rst
│   ├── faq.rst
│   ├── index.rst
│   ├── install.rst
│   ├── make.bat
│   ├── requirements.txt
│   ├── roadmap.rst
│   ├── robots.txt
│   ├── service.rst
│   └── type.rst
├── make_doc.sh
├── requirements.txt
├── scripts/
│   ├── lang.py
│   └── package_script.py
├── setup.py
├── tests/
│   ├── change_pw.py
│   ├── comment.py
│   ├── config.py
│   ├── exceptions.py
│   ├── get_board_info.py
│   ├── get_board_list.py
│   ├── get_bottom_post_list.py
│   ├── get_favourite_boards.py
│   ├── get_mail.py
│   ├── get_newest_index.py
│   ├── get_post.py
│   ├── get_time.py
│   ├── get_user.py
│   ├── give_p.py
│   ├── i18n.py
│   ├── init.py
│   ├── logger.py
│   ├── login_logout.py
│   ├── performance.py
│   ├── post.py
│   ├── reply.py
│   ├── search_user.py
│   ├── service.py
│   └── util.py
└── upload.sh
Download .txt
SYMBOL INDEX (299 symbols across 60 files)

FILE: PyPtt/PTT.py
  class API (line 37) | class API:
    method __init__ (line 38) | def __init__(self, **kwargs):
    method __del__ (line 198) | def __del__(self):
    method login (line 202) | def login(self, ptt_id: str, ptt_pw: str, kick_other_session: bool = F...
    method logout (line 242) | def logout(self) -> None:
    method get_time (line 264) | def get_time(self) -> str:
    method get_post (line 287) | def get_post(self, board: str, aid: Optional[str] = None, index: Optio...
    method get_newest_index (line 354) | def get_newest_index(self, index_type: data_type.NewIndex, board: Opti...
    method post (line 422) | def post(self, board: str, title_index: int, title: str, content: str,...
    method comment (line 458) | def comment(self, board: str, comment_type: data_type.CommentType, con...
    method get_user (line 499) | def get_user(self, user_id: str) -> Dict:
    method give_money (line 532) | def give_money(self, ptt_id: str, money: int, red_bag_title: Optional[...
    method mail (line 573) | def mail(self, ptt_id: str, title: str, content: str, sign_file: [int ...
    method get_all_boards (line 609) | def get_all_boards(self) -> List[str]:
    method reply_post (line 635) | def reply_post(self, reply_to: data_type.ReplyTo, board: str, content:...
    method set_board_title (line 676) | def set_board_title(self, board: str, new_title: str) -> None:
    method mark_post (line 709) | def mark_post(self, mark_type: int, board: str, aid: Optional[str] = N...
    method get_favourite_boards (line 747) | def get_favourite_boards(self) -> List[dict]:
    method bucket (line 773) | def bucket(self, board: str, bucket_days: int, reason: str, ptt_id: st...
    method search_user (line 809) | def search_user(self, ptt_id: str, min_page: Optional[int] = None, max...
    method get_board_info (line 840) | def get_board_info(self, board: str, get_post_types: bool = False) -> ...
    method get_mail (line 871) | def get_mail(self, index: int, search_type: Optional[data_type.SearchT...
    method del_mail (line 908) | def del_mail(self, index: int) -> None:
    method change_pw (line 940) | def change_pw(self, new_password: str) -> None:
    method get_aid_from_url (line 972) | def get_aid_from_url(self, url: str) -> Tuple[str, str]:
    method get_bottom_post_list (line 993) | def get_bottom_post_list(self, board: str) -> List[str]:
    method del_post (line 1023) | def del_post(self, board: str, aid: Optional[str] = None, index: int =...
    method fast_post_step0 (line 1057) | def fast_post_step0(self, board: str, title: str, content: str, post_t...
    method fast_post_step1 (line 1060) | def fast_post_step1(self, sign_file):

FILE: PyPtt/_api_bucket.py
  function bucket (line 11) | def bucket(api, board: str, bucket_days: int, reason: str, ptt_id: str) ...

FILE: PyPtt/_api_call_status.py
  function get_call_status (line 9) | def get_call_status(api) -> None:
  function set_call_status (line 51) | def set_call_status(api, call_status) -> None:

FILE: PyPtt/_api_change_pw.py
  function change_pw (line 8) | def change_pw(api, new_password: str) -> None:

FILE: PyPtt/_api_comment.py
  function _comment (line 23) | def _comment(api,
  function comment (line 122) | def comment(api, board: str, push_type: data_type.CommentType, push_cont...

FILE: PyPtt/_api_del_post.py
  function del_post (line 17) | def del_post(api, board: str, post_aid: Optional[str] = None, post_index...

FILE: PyPtt/_api_get_board_info.py
  function get_board_info (line 15) | def get_board_info(api, board: str, get_post_kind: bool, call_by_others:...

FILE: PyPtt/_api_get_board_list.py
  function get_board_list (line 12) | def get_board_list(api) -> list:

FILE: PyPtt/_api_get_bottom_post_list.py
  function get_bottom_post_list (line 12) | def get_bottom_post_list(api, board):

FILE: PyPtt/_api_get_favourite_board.py
  function get_favourite_board (line 10) | def get_favourite_board(api) -> list:

FILE: PyPtt/_api_get_newest_index.py
  function _get_newest_index (line 17) | def _get_newest_index(api) -> int:
  function get_newest_index (line 56) | def get_newest_index(api, index_type: data_type.NewIndex, board: Optiona...

FILE: PyPtt/_api_get_post.py
  function get_post (line 23) | def get_post(api, board: str, aid: Optional[str] = None, index: Optional...
  function _get_post (line 94) | def _get_post(api, board: str, post_aid: Optional[str] = None, post_inde...

FILE: PyPtt/_api_get_post_index.py
  function get_post_index (line 10) | def get_post_index(api, board: str, aid: str) -> int:

FILE: PyPtt/_api_get_time.py
  function get_time (line 14) | def get_time(api) -> str:

FILE: PyPtt/_api_get_user.py
  function get_user (line 20) | def get_user(api, ptt_id: str) -> Dict:

FILE: PyPtt/_api_give_money.py
  function give_money (line 11) | def give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag...

FILE: PyPtt/_api_has_new_mail.py
  function has_new_mail (line 10) | def has_new_mail(api) -> int:

FILE: PyPtt/_api_loginout.py
  function logout (line 14) | def logout(api) -> None:
  function login (line 48) | def login(api, ptt_id: str, ptt_pw: str, kick_other_session: bool):

FILE: PyPtt/_api_mail.py
  function mail (line 20) | def mail(api,
  function get_mail (line 119) | def get_mail(api, index: int, search_type: Optional[data_type.SearchType...
  function del_mail (line 272) | def del_mail(api, index) -> None:

FILE: PyPtt/_api_mark_post.py
  function mark_post (line 13) | def mark_post(api, mark_type: int, board: str, post_aid: str, post_index...

FILE: PyPtt/_api_post.py
  function fast_post_step0 (line 13) | def fast_post_step0(api, board: str, title: str, content: str, post_type...
  function fast_post_step1 (line 41) | def fast_post_step1(api: object, sign_file) -> None:
  function fast_post (line 57) | def fast_post(
  function post (line 95) | def post(api, board: str, title: str, content: str, title_index: int, si...

FILE: PyPtt/_api_reply_post.py
  function reply_post (line 12) | def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: st...

FILE: PyPtt/_api_search_user.py
  function search_user (line 11) | def search_user(api, ptt_id: str, min_page: int, max_page: int) -> list:

FILE: PyPtt/_api_set_board_title.py
  function set_board_title (line 10) | def set_board_title(api, board: str, new_title: str) -> None:

FILE: PyPtt/_api_util.py
  function get_content (line 17) | def get_content(api, post_mode: bool = True):
  function get_mailbox_capacity (line 185) | def get_mailbox_capacity(api) -> tuple[int, int]:
  function parse_query_post (line 215) | def parse_query_post(api, ori_screen):
  function get_search_condition_cmd (line 340) | def get_search_condition_cmd(index_type: data_type.NewIndex, search_list...
  function goto_board (line 369) | def goto_board(api, board: str, refresh: bool = False, end: bool = False...
  function one_thread (line 409) | def one_thread(api):
  function check_board (line 416) | def check_board(api, board: str, check_moderator: bool = False) -> Dict:

FILE: PyPtt/check_value.py
  function check_type (line 5) | def check_type(value, value_type, name) -> None:
  function check_range (line 17) | def check_range(value, min_value, max_value, name) -> None:
  function check_index (line 27) | def check_index(name, index, max_value=None) -> None:
  function check_index_range (line 39) | def check_index_range(start_name, start_index, end_name, end_index, max_...

FILE: PyPtt/config.py
  class Config (line 5) | class Config:

FILE: PyPtt/connect_core.py
  class TargetUnit (line 29) | class TargetUnit:
    method __init__ (line 30) | def __init__(self, detect_target, log_level: log.LogLevel = None, resp...
    method is_match (line 49) | def is_match(self, screen: str) -> bool:
    method get_detect_target (line 64) | def get_detect_target(self):
    method get_log_level (line 67) | def get_log_level(self):
    method get_response (line 70) | def get_response(self, screen: str) -> str:
    method is_break (line 75) | def is_break(self) -> bool:
    method raise_exception (line 78) | def raise_exception(self):
    method is_refresh (line 82) | def is_refresh(self) -> bool:
    method is_break_after_send (line 85) | def is_break_after_send(self) -> bool:
    method is_secret (line 88) | def is_secret(self) -> bool:
  class RecvData (line 92) | class RecvData:
    method __init__ (line 93) | def __init__(self):
  function websocket_recv_func (line 97) | async def websocket_recv_func(core, recv_data_obj):
  function websocket_receiver (line 101) | async def websocket_receiver(core, screen_timeout, recv_data_obj):
  class ReceiveDataQueue (line 108) | class ReceiveDataQueue(object):
    method __init__ (line 109) | def __init__(self):
    method add (line 112) | def add(self, screen):
    method get (line 116) | def get(self, last=1):
  class API (line 120) | class API(object):
    method __init__ (line 121) | def __init__(self, config):
    method connect (line 129) | def connect(self) -> None:
    method _decode_screen (line 207) | def _decode_screen(self, receive_data_buffer, start_time, target_list,...
    method send (line 260) | def send(self, msg: str, target_list: list, screen_timeout: int = 0, r...
    method close (line 387) | def close(self):
    method get_screen_queue (line 393) | def get_screen_queue(self) -> list:

FILE: PyPtt/data_type.py
  class Language (line 7) | class Language:
  class ConnectMode (line 12) | class ConnectMode(AutoStrEnum):
  class SearchType (line 17) | class SearchType(AutoStrEnum):
  class ReplyTo (line 33) | class ReplyTo(AutoStrEnum):
  class CommentType (line 41) | class CommentType(AutoStrEnum):
  class UserField (line 47) | class UserField(AutoStrEnum):
  class CommentField (line 63) | class CommentField(AutoStrEnum):
  class PostStatus (line 71) | class PostStatus(AutoStrEnum):
  class PostField (line 78) | class PostField(AutoStrEnum):
  class Cursor (line 109) | class Cursor:
  class NewIndex (line 116) | class NewIndex(AutoStrEnum):
  class HOST (line 125) | class HOST(AutoStrEnum):
  class MarkType (line 134) | class MarkType(AutoStrEnum):
  class FavouriteBoardField (line 147) | class FavouriteBoardField(AutoStrEnum):
  class MailField (line 153) | class MailField(AutoStrEnum):
  class BoardField (line 164) | class BoardField(AutoStrEnum):
  class Compare (line 190) | class Compare(AutoStrEnum):
  class TimedDict (line 197) | class TimedDict:
    method __init__ (line 198) | def __init__(self, timeout: int = 0):
    method __setitem__ (line 203) | def __setitem__(self, key, value):
    method __getitem__ (line 207) | def __getitem__(self, key):
    method __contains__ (line 217) | def __contains__(self, key):
    method __len__ (line 225) | def __len__(self):
    method cleanup (line 229) | def cleanup(self):

FILE: PyPtt/exceptions.py
  class Error (line 5) | class Error(Exception):
  class UnknownError (line 9) | class UnknownError(Error):
    method __init__ (line 10) | def __init__(self, message):
    method __str__ (line 13) | def __str__(self):
  class RequireLogin (line 17) | class RequireLogin(Error):
    method __init__ (line 18) | def __init__(self, message):
    method __str__ (line 21) | def __str__(self):
  class NoPermission (line 25) | class NoPermission(Error):
    method __init__ (line 26) | def __init__(self, message):
    method __str__ (line 29) | def __str__(self):
  class LoginError (line 33) | class LoginError(Error):
    method __init__ (line 34) | def __init__(self):
    method __str__ (line 37) | def __str__(self):
  class NoFastComment (line 41) | class NoFastComment(Error):
    method __init__ (line 42) | def __init__(self):
    method __str__ (line 45) | def __str__(self):
  class NoSuchUser (line 49) | class NoSuchUser(Error):
    method __init__ (line 50) | def __init__(self, user):
    method __str__ (line 53) | def __str__(self):
  class NoSuchMail (line 57) | class NoSuchMail(Error):
    method __init__ (line 58) | def __init__(self):
    method __str__ (line 61) | def __str__(self):
  class NoMoney (line 81) | class NoMoney(Error):
    method __init__ (line 82) | def __init__(self):
    method __str__ (line 85) | def __str__(self):
  class NoSuchBoard (line 89) | class NoSuchBoard(Error):
    method __init__ (line 90) | def __init__(self, config, board):
    method __str__ (line 107) | def __str__(self):
  class ConnectionClosed (line 111) | class ConnectionClosed(Error):
    method __init__ (line 112) | def __init__(self):
    method __str__ (line 115) | def __str__(self):
  class UnregisteredUser (line 119) | class UnregisteredUser(Error):
    method __init__ (line 120) | def __init__(self, api_name):
    method __str__ (line 123) | def __str__(self):
  class MultiThreadOperated (line 127) | class MultiThreadOperated(Error):
    method __init__ (line 128) | def __init__(self):
    method __str__ (line 131) | def __str__(self):
  class WrongIDorPassword (line 135) | class WrongIDorPassword(Error):
    method __init__ (line 136) | def __init__(self):
    method __str__ (line 139) | def __str__(self):
  class WrongPassword (line 143) | class WrongPassword(Error):
    method __init__ (line 144) | def __init__(self):
    method __str__ (line 147) | def __str__(self):
  class LoginTooOften (line 151) | class LoginTooOften(Error):
    method __init__ (line 152) | def __init__(self):
    method __str__ (line 155) | def __str__(self):
  class UseTooManyResources (line 159) | class UseTooManyResources(Error):
    method __init__ (line 160) | def __init__(self):
    method __str__ (line 163) | def __str__(self):
  class HostNotSupport (line 167) | class HostNotSupport(Error):
    method __init__ (line 168) | def __init__(self, api):
    method __str__ (line 171) | def __str__(self):
  class CantComment (line 175) | class CantComment(Error):
    method __init__ (line 176) | def __init__(self):
    method __str__ (line 179) | def __str__(self):
  class CantResponse (line 183) | class CantResponse(Error):
    method __init__ (line 184) | def __init__(self):
    method __str__ (line 187) | def __str__(self):
  class NeedModeratorPermission (line 191) | class NeedModeratorPermission(Error):
    method __init__ (line 192) | def __init__(self, board):
    method __str__ (line 195) | def __str__(self):
  class ConnectError (line 199) | class ConnectError(Error):
    method __init__ (line 200) | def __init__(self, config):
    method __str__ (line 203) | def __str__(self):
  class NoSuchPost (line 207) | class NoSuchPost(Error):
    method __init__ (line 208) | def __init__(self, board, aid):
    method __str__ (line 214) | def __str__(self):
  class CanNotUseSearchPostCode (line 218) | class CanNotUseSearchPostCode(Error):
    method __init__ (line 223) | def __init__(self):
    method __str__ (line 226) | def __str__(self):
  class UserHasPreviouslyBeenBanned (line 230) | class UserHasPreviouslyBeenBanned(Error):
    method __init__ (line 231) | def __init__(self):
    method __str__ (line 234) | def __str__(self):
  class MailboxFull (line 238) | class MailboxFull(Error):
    method __init__ (line 239) | def __init__(self):
    method __str__ (line 242) | def __str__(self):
  class NoSearchResult (line 246) | class NoSearchResult(Error):
    method __init__ (line 247) | def __init__(self):
    method __str__ (line 250) | def __str__(self):
  class OnlySecureConnection (line 256) | class OnlySecureConnection(Error):
    method __init__ (line 257) | def __init__(self):
    method __str__ (line 260) | def __str__(self):
  class SetContactMailFirst (line 264) | class SetContactMailFirst(Error):
    method __init__ (line 265) | def __init__(self):
    method __str__ (line 268) | def __str__(self):
  class ResetYourContactEmail (line 272) | class ResetYourContactEmail(Error):
    method __init__ (line 273) | def __init__(self):
    method __str__ (line 276) | def __str__(self):

FILE: PyPtt/i18n.py
  function replace (line 22) | def replace(string, *args):
  function init (line 29) | def init(locale: str, cache: bool = False) -> None:

FILE: PyPtt/lib_util.py
  function get_file_name (line 19) | def get_file_name(path_str: str) -> str:
  function get_current_func_name (line 25) | def get_current_func_name() -> str:
  function findnth (line 29) | def findnth(haystack, needle, n) -> int:
  function get_random_str (line 36) | def get_random_str(length) -> str:
  function get_aid_from_url (line 45) | def get_aid_from_url(url: str) -> Tuple[str, str]:
  function sync_version (line 88) | def sync_version() -> Tuple[data_type.Compare, str]:
  function uniform_new_line (line 138) | def uniform_new_line(text: str) -> str:
  function check_aid (line 149) | def check_aid(aid: str) -> str:

FILE: PyPtt/log.py
  class LogLv (line 5) | class LogLv:
    method __init__ (line 8) | def __init__(self, level):
    method level (line 12) | def level(self):
    method __eq__ (line 15) | def __eq__(self, other):
  class LogLevel (line 26) | class LogLevel:
  function _combine_msg (line 41) | def _combine_msg(*args) -> str:
  class Logger (line 61) | class Logger:
    method __init__ (line 64) | def __init__(self, name: str, level: int = logging.NOTSET, logger_call...
    method info (line 79) | def info(self, *args):
    method debug (line 89) | def debug(self, *args):
  function init (line 103) | def init(log_level: LogLv, name: Optional[str] = None, logger_callback: ...

FILE: PyPtt/screens.py
  class Target (line 11) | class Target:
  function show (line 165) | def show(config, screen_queue, function_name=None):
  class VT100Parser (line 198) | class VT100Parser:
    method _h (line 199) | def _h(self):
    method _2j (line 203) | def _2j(self):
    method _move (line 207) | def _move(self, x, y):
    method _newline (line 211) | def _newline(self):
    method _k (line 215) | def _k(self):
    method __init__ (line 221) | def __init__(self, bytes_data, encoding):

FILE: PyPtt/service.py
  class Service (line 11) | class Service:
    method __init__ (line 13) | def __init__(self, pyptt_init_config: Optional[dict] = None):
    method _run (line 91) | def _run(self):
    method _get_call_id (line 122) | def _get_call_id(self):
    method call (line 131) | def call(self, api: str, args: Optional[dict] = None):
    method close (line 163) | def close(self):

FILE: scripts/lang.py
  function add_lang (line 13) | def add_lang():
  function check_lang (line 28) | def check_lang():

FILE: scripts/package_script.py
  function get_next_version (line 6) | def get_next_version():

FILE: setup.py
  function version_automation_script (line 8) | def version_automation_script():

FILE: tests/change_pw.py
  function test (line 10) | def test(ptt_bot: PyPtt.API):
  function func (line 14) | def func():

FILE: tests/comment.py
  function test (line 10) | def test(ptt_bot):
  function func (line 58) | def func():

FILE: tests/get_board_info.py
  function test (line 11) | def test(ptt_bot: PyPtt.API):
  function func (line 21) | def func():

FILE: tests/get_board_list.py
  function test (line 12) | def test(ptt_bot: PyPtt.API):
  function func (line 19) | def func():

FILE: tests/get_bottom_post_list.py
  function test (line 10) | def test(ptt_bot: PyPtt.API):
  function func (line 16) | def func():

FILE: tests/get_favourite_boards.py
  function test (line 10) | def test(ptt_bot: PyPtt.API):
  function func (line 17) | def func():

FILE: tests/get_mail.py
  function test (line 11) | def test(ptt_bot: PyPtt.API):
  function func (line 20) | def func():

FILE: tests/get_newest_index.py
  function test_board_index (line 11) | def test_board_index(ptt_bot: PyPtt.API):
  function test_mail_index (line 42) | def test_mail_index(ptt_bot: PyPtt.API):
  function func (line 49) | def func():

FILE: tests/get_post.py
  function test_no_condition (line 13) | def test_no_condition(ptt_bot: PyPtt.API):
  function get_post_with_condition (line 71) | def get_post_with_condition(ptt_bot: PyPtt.API):
  function test (line 144) | def test(ptt_bot: PyPtt.API):
  function func (line 154) | def func():

FILE: tests/get_time.py
  function test (line 11) | def test(ptt_bot: PyPtt.API):
  function func (line 20) | def func():

FILE: tests/get_user.py
  function test (line 11) | def test(ptt_bot: PyPtt.API):
  function func (line 22) | def func():

FILE: tests/give_p.py
  function test (line 10) | def test(ptt_bot: PyPtt.API):
  function func (line 14) | def func():

FILE: tests/i18n.py
  function test (line 11) | def test():

FILE: tests/init.py
  function test (line 9) | def test():

FILE: tests/logger.py
  function func (line 8) | def func():

FILE: tests/login_logout.py
  function test (line 11) | def test(ptt_bot: PyPtt.API):
  function func (line 27) | def func():

FILE: tests/performance.py
  function test (line 11) | def test(ptt_bot):
  function func (line 28) | def func():

FILE: tests/post.py
  function test (line 15) | def test(ptt_bot: PyPtt.API):
  function func (line 120) | def func():

FILE: tests/reply.py
  function test (line 15) | def test(ptt_bot: PyPtt.API):
  function func (line 91) | def func():

FILE: tests/search_user.py
  function test (line 11) | def test(ptt_bot: PyPtt.API):
  function func (line 22) | def func():

FILE: tests/service.py
  function api_test (line 12) | def api_test(thread_id, service):
  function test (line 23) | def test():

FILE: tests/util.py
  function log_to_file (line 9) | def log_to_file(msg: str):
  function get_id_pw (line 14) | def get_id_pw(password_file):
  function login (line 28) | def login(ptt_bot: PyPtt.API, kick: bool = True):
  function show_data (line 55) | def show_data(data, key: str = None):
  function del_all_post (line 60) | def del_all_post(ptt_bot: PyPtt.API):
Condensed preview — 119 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (379K chars).
[
  {
    "path": ".github/workflows/deploy.yml",
    "chars": 1842,
    "preview": "# This workflow will install Python dependencies, run tests and lint with a variety of Python versions\n# For more inform"
  },
  {
    "path": ".github/workflows/docs.yml",
    "chars": 786,
    "preview": "\nname: docs\n\n# run on merge to master or manual trigger\non:\n  pull_request:\n    types: [ closed ]\n    paths:\n      - 'do"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1921,
    "preview": "# This workflow will install Python dependencies, run tests and lint with a single version of Python\n# For more informat"
  },
  {
    "path": ".gitignore",
    "chars": 323,
    "preview": "__pycache__/\nbuild/\ndist/\n/CrawlBoardResult.txt\n/.pypirc\nPTTLibrary.egg-info/\n*Out.txt\n/Big5Data.txt\nPTTLibrary-*/\n/PTTL"
  },
  {
    "path": "GourceScript.bat",
    "chars": 66,
    "preview": "@echo off\ncls\n\ngource --seconds-per-day 0.05 --title \"PTT Library\""
  },
  {
    "path": "LICENSE",
    "chars": 7652,
    "preview": "                   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007"
  },
  {
    "path": "MANIFEST.in",
    "chars": 85,
    "preview": "include LICENSE\ninclude README.md\n\n# Include the data files\nrecursive-include PyPtt *"
  },
  {
    "path": "PyPtt/PTT.py",
    "chars": 29073,
    "preview": "from __future__ import annotations\n\nimport functools\nimport threading\nfrom typing import Dict, Tuple, Callable, List, O"
  },
  {
    "path": "PyPtt/__init__.py",
    "chars": 195,
    "preview": "__version__ = '1.1.2'\n\nfrom .PTT import API\nfrom .data_type import *\nfrom .exceptions import *\nfrom .log import LogLevel"
  },
  {
    "path": "PyPtt/_api_bucket.py",
    "chars": 1910,
    "preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import excepti"
  },
  {
    "path": "PyPtt/_api_call_status.py",
    "chars": 2177,
    "preview": "from . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import log\nfrom"
  },
  {
    "path": "PyPtt/_api_change_pw.py",
    "chars": 1604,
    "preview": "from . import command, _api_util\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import lo"
  },
  {
    "path": "PyPtt/_api_comment.py",
    "chars": 6902,
    "preview": "import collections\nimport time\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import co"
  },
  {
    "path": "PyPtt/_api_del_post.py",
    "chars": 3279,
    "preview": "from __future__ import annotations\n\nfrom typing import Optional\n\nfrom . import _api_util\nfrom . import check_value\nfrom "
  },
  {
    "path": "PyPtt/_api_get_board_info.py",
    "chars": 8026,
    "preview": "import re\nfrom typing import Dict\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import"
  },
  {
    "path": "PyPtt/_api_get_board_list.py",
    "chars": 3098,
    "preview": "import progressbar\n\nfrom . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfr"
  },
  {
    "path": "PyPtt/_api_get_bottom_post_list.py",
    "chars": 2246,
    "preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_ty"
  },
  {
    "path": "PyPtt/_api_get_favourite_board.py",
    "chars": 2557,
    "preview": "from . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfro"
  },
  {
    "path": "PyPtt/_api_get_newest_index.py",
    "chars": 7267,
    "preview": "from __future__ import annotations\n\nimport re\nfrom typing import Optional\n\nfrom . import _api_util\nfrom . import check_v"
  },
  {
    "path": "PyPtt/_api_get_post.py",
    "chars": 20967,
    "preview": "from __future__ import annotations\n\nimport json\nimport re\nimport time\nfrom typing import Dict, Optional\n\nfrom AutoStrEnu"
  },
  {
    "path": "PyPtt/_api_get_post_index.py",
    "chars": 2186,
    "preview": "from . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfro"
  },
  {
    "path": "PyPtt/_api_get_time.py",
    "chars": 1084,
    "preview": "import re\n\nfrom . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . impo"
  },
  {
    "path": "PyPtt/_api_get_user.py",
    "chars": 4340,
    "preview": "import json\nimport re\nfrom typing import Dict\n\nfrom AutoStrEnum import AutoJsonEncoder\n\nfrom . import _api_util\nfrom . i"
  },
  {
    "path": "PyPtt/_api_give_money.py",
    "chars": 3812,
    "preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import excepti"
  },
  {
    "path": "PyPtt/_api_has_new_mail.py",
    "chars": 2087,
    "preview": "import re\n\nfrom . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import log\nfrom . import scre"
  },
  {
    "path": "PyPtt/_api_loginout.py",
    "chars": 7423,
    "preview": "import re\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . imp"
  },
  {
    "path": "PyPtt/_api_mail.py",
    "chars": 8912,
    "preview": "from __future__ import annotations\n\nimport re\nfrom typing import Dict, Optional\n\nfrom . import _api_util\nfrom . import c"
  },
  {
    "path": "PyPtt/_api_mark_post.py",
    "chars": 4419,
    "preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_ty"
  },
  {
    "path": "PyPtt/_api_post.py",
    "chars": 5220,
    "preview": "from __future__ import annotations\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . impor"
  },
  {
    "path": "PyPtt/_api_reply_post.py",
    "chars": 3510,
    "preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_ty"
  },
  {
    "path": "PyPtt/_api_search_user.py",
    "chars": 2812,
    "preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import excepti"
  },
  {
    "path": "PyPtt/_api_set_board_title.py",
    "chars": 1259,
    "preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import excepti"
  },
  {
    "path": "PyPtt/_api_util.py",
    "chars": 15142,
    "preview": "from __future__ import annotations\n\nimport functools\nimport re\nimport threading\nfrom typing import Dict, Optional\n\nfrom "
  },
  {
    "path": "PyPtt/check_value.py",
    "chars": 2311,
    "preview": "from . import i18n\nfrom . import log\n\n\ndef check_type(value, value_type, name) -> None:\n    if not isinstance(value, val"
  },
  {
    "path": "PyPtt/command.py",
    "chars": 515,
    "preview": "# http://www.physics.udel.edu/~watson/scen103/ascii.html\nenter = '\\r'\ntab = '\\t'\nctrl_c = '\\x03'\nctrl_d = '\\x04'\nctrl_e "
  },
  {
    "path": "PyPtt/config.py",
    "chars": 740,
    "preview": "from . import data_type\nfrom . import log\n\n\nclass Config:\n    # retry_wait_time 秒後重新連線\n    retry_wait_time = 3\n\n    # Sc"
  },
  {
    "path": "PyPtt/connect_core.py",
    "chars": 13838,
    "preview": "from __future__ import annotations\n\nimport asyncio\nimport ssl\nimport telnetlib\nimport threading\nimport time\nimport trace"
  },
  {
    "path": "PyPtt/data_type.py",
    "chars": 4874,
    "preview": "import time\nfrom enum import auto\n\nfrom AutoStrEnum import AutoStrEnum\n\n\nclass Language:\n    MANDARIN = 'zh_TW'\n    ENGL"
  },
  {
    "path": "PyPtt/exceptions.py",
    "chars": 5523,
    "preview": "from . import data_type\nfrom . import i18n\n\n\nclass Error(Exception):\n    pass\n\n\nclass UnknownError(Error):\n    def __ini"
  },
  {
    "path": "PyPtt/i18n.py",
    "chars": 1341,
    "preview": "import os\nimport random\n\nimport yaml\n\nfrom . import __version__\nfrom . import data_type\n\nlocale_pool = {\n    data_type.L"
  },
  {
    "path": "PyPtt/lang/en_US.yaml",
    "chars": 4094,
    "preview": "PTT: PTT\nPTT2: PTT2\nactive: Active\nauthor: Author\nboard: Board\ncan_not_use_search_post_code_f: This status can not use t"
  },
  {
    "path": "PyPtt/lang/zh_TW.yaml",
    "chars": 2691,
    "preview": "PTT: 批踢踢\nPTT2: 批踢踢兔\nactive: 啟動\nauthor: 作者\nboard: 看板\ncan_not_use_search_post_code_f: 此狀態下無法使用搜尋文章代碼(AID)功能\ncatch_bottom_p"
  },
  {
    "path": "PyPtt/lib_util.py",
    "chars": 4488,
    "preview": "import functools\nimport os\nimport random\nimport re\nimport string\nimport time\nimport traceback\nfrom typing import Tuple\n\n"
  },
  {
    "path": "PyPtt/log.py",
    "chars": 2895,
    "preview": "import logging\nfrom typing import Optional\n\n\nclass LogLv:\n    _level: int\n\n    def __init__(self, level):\n        self._"
  },
  {
    "path": "PyPtt/screens.py",
    "chars": 54238,
    "preview": "import re\nimport sys\n\nfrom uao import register_uao\n\nfrom . import log\n\nregister_uao()\n\n\nclass Target:\n    MainMenu = [\n "
  },
  {
    "path": "PyPtt/service.py",
    "chars": 4496,
    "preview": "import threading\nimport time\nimport uuid\nfrom typing import Optional\n\nfrom . import PTT\nfrom . import check_value\nfrom ."
  },
  {
    "path": "README.md",
    "chars": 1551,
    "preview": "![](https://raw.githubusercontent.com/PttCodingMan/PyPtt/master/logo/facebook_cover_photo_2.png)\n# PyPtt\n[![Package Vers"
  },
  {
    "path": "docs/CNAME",
    "chars": 8,
    "preview": "pyptt.cc"
  },
  {
    "path": "docs/Makefile",
    "chars": 634,
    "preview": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the "
  },
  {
    "path": "docs/api/bucket.rst",
    "chars": 94,
    "preview": "bucket\n==========\n\n.. _api-bucket:\n\n.. automodule:: PyPtt.API\n   :members: bucket\n   :noindex:"
  },
  {
    "path": "docs/api/change_pw.rst",
    "chars": 87,
    "preview": "change_pw\n==============\n\n.. automodule:: PyPtt.API\n   :members: change_pw\n   :noindex:"
  },
  {
    "path": "docs/api/comment.rst",
    "chars": 97,
    "preview": "comment\n==========\n\n.. _api-comment:\n\n.. automodule:: PyPtt.API\n   :members: comment\n   :noindex:"
  },
  {
    "path": "docs/api/del_mail.rst",
    "chars": 102,
    "preview": "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",
    "chars": 85,
    "preview": "del_post\n==============\n\n.. automodule:: PyPtt.API\n   :members: del_post\n   :noindex:"
  },
  {
    "path": "docs/api/get_aid_from_url.rst",
    "chars": 108,
    "preview": "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",
    "chars": 100,
    "preview": "get_all_boards\n=================\n\n.. automodule:: PyPtt.API\n   :members: get_all_boards\n   :noindex:"
  },
  {
    "path": "docs/api/get_board_info.rst",
    "chars": 125,
    "preview": "get_board_info\n=================\n\n.. _api-get-board-info:\n\n.. automodule:: PyPtt.API\n   :members: get_board_info\n   :noi"
  },
  {
    "path": "docs/api/get_bottom_post_list.rst",
    "chars": 117,
    "preview": "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",
    "chars": 152,
    "preview": "get_favourite_boards\n==========================\n\n.. _api-get-favourite-boards:\n\n.. automodule:: PyPtt.API\n   :members: g"
  },
  {
    "path": "docs/api/get_mail.rst",
    "chars": 102,
    "preview": "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",
    "chars": 131,
    "preview": "get_newest_index\n=================\n\n.. _api-get-newest-index:\n\n.. automodule:: PyPtt.API\n   :members: get_newest_index\n "
  },
  {
    "path": "docs/api/get_post.rst",
    "chars": 100,
    "preview": "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",
    "chars": 100,
    "preview": "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",
    "chars": 100,
    "preview": "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",
    "chars": 109,
    "preview": "give_money\n=============\n\n.. _api-give-money:\n\n.. automodule:: PyPtt.API\n   :members: give_money\n   :noindex:"
  },
  {
    "path": "docs/api/index.rst",
    "chars": 654,
    "preview": "APIs\n=============\n| 這是 PyPtt 的 API 文件。\n| 我們在這裡介紹 PyPtt 目前所有支援 PTT, PTT2 的功能。\n\n基本功能\n----------------\n.. toctree::\n\n   in"
  },
  {
    "path": "docs/api/init.rst",
    "chars": 76,
    "preview": "init\n=======\n\n.. _api-init:\n\n.. automodule:: PyPtt.API\n   :members: __init__"
  },
  {
    "path": "docs/api/login_logout.rst",
    "chars": 120,
    "preview": "login, logout\n================\n\n.. _api-login-logout:\n\n.. automodule:: PyPtt.API\n   :members: login, logout\n   :noindex:"
  },
  {
    "path": "docs/api/mail.rst",
    "chars": 91,
    "preview": "mail\n=============\n\n.. _api-mail:\n\n.. automodule:: PyPtt.API\n   :members: mail\n   :noindex:"
  },
  {
    "path": "docs/api/mark_post.rst",
    "chars": 108,
    "preview": "mark_post\n===============\n\n.. _api-mark-post:\n\n.. automodule:: PyPtt.API\n   :members: mark_post\n   :noindex:"
  },
  {
    "path": "docs/api/post.rst",
    "chars": 88,
    "preview": "post\n==========\n\n.. _api-post:\n\n.. automodule:: PyPtt.API\n   :members: post\n   :noindex:"
  },
  {
    "path": "docs/api/reply_post.rst",
    "chars": 106,
    "preview": "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",
    "chars": 115,
    "preview": "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",
    "chars": 132,
    "preview": "set_board_title\n=====================\n\n.. _api-set-board-title:\n\n.. automodule:: PyPtt.API\n   :members: set_board_title\n"
  },
  {
    "path": "docs/changelog.rst",
    "chars": 462,
    "preview": "更新日誌\n====================\n| 這裡寫著 PyPtt 的故事。\n|\n| 2022.12.20 PyPtt 1.0.3,logger 改採用以 logging_ 為基底。\n\n.. _logging: https://d"
  },
  {
    "path": "docs/conf.py",
    "chars": 1552,
    "preview": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see t"
  },
  {
    "path": "docs/dev.rst",
    "chars": 1302,
    "preview": "Development\n================\n如果你想參與開發,請參考以下須知:\n\n開發環境\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n我們建議您使用 virt"
  },
  {
    "path": "docs/docker.rst",
    "chars": 2708,
    "preview": "Docker Image\n=================\n\n.. image:: https://img.shields.io/docker/v/codingman000/pyptt/latest\n   :target: https:/"
  },
  {
    "path": "docs/examples.rst",
    "chars": 3228,
    "preview": "使用範例\n=============\n| 這裡記錄了各種實際使用的範例 ☺️\n\n保持登入\n--------\n這裡示範了如何保持登入\n\n.. code-block:: python\n\n    import PyPtt\n\n    def log"
  },
  {
    "path": "docs/exceptions.rst",
    "chars": 2518,
    "preview": "例外\n========\n| 這裡介紹 PyPtt 的例外。\n| 可以用 try...except... 來處理。\n\n| 例外的種類\n\n.. py:exception:: PyPtt.exceptions.RequireLogin\n    :"
  },
  {
    "path": "docs/faq.rst",
    "chars": 1947,
    "preview": "FAQ\n==========\n這裡搜集了一些常見問題的解答,如果你有任何問題,請先看看這裡。\n\nQ: 我該如何使用 PyPtt?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
  },
  {
    "path": "docs/index.rst",
    "chars": 2561,
    "preview": "PyPtt\n====================\n\n.. image:: _static/logo_cover.png\n    :alt: PyPtt: PTT bot library for Python\n    :align: ce"
  },
  {
    "path": "docs/install.rst",
    "chars": 1597,
    "preview": "安裝 PyPtt\n===================\n\nPython 版本\n--------------\n| 推薦使用 CPython_ 3.8+。\n\n.. _CPython: https://www.python.org/\n\n相依套件"
  },
  {
    "path": "docs/make.bat",
    "chars": 800,
    "preview": "@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=sp"
  },
  {
    "path": "docs/requirements.txt",
    "chars": 67,
    "preview": "PyPtt\nsphinx\nsphinx-copybutton\npygments==2.15.0\nFuro\nsphinx-sitemap"
  },
  {
    "path": "docs/roadmap.rst",
    "chars": 735,
    "preview": "開發\n=============\n| 這裡列了一些我們正在開發的功能,如果你有任何建議,歡迎找我們聊聊。\n| 或者你也可以直接在 github 上開 issue,我們會盡快回覆。\n\n| 當然如果你有興趣參與開發,也歡迎你加入我們,我們會盡快"
  },
  {
    "path": "docs/robots.txt",
    "chars": 52,
    "preview": "User-agent: *\n\nSitemap: https://pyptt.cc/sitemap.xml"
  },
  {
    "path": "docs/service.rst",
    "chars": 95,
    "preview": "Service\n===========\n\n\n.. automodule:: PyPtt.Service\n    :members: __init__\n    :undoc-members:\n"
  },
  {
    "path": "docs/type.rst",
    "chars": 6714,
    "preview": "參數型態\n===========\n這裡介紹 PyPtt 的參數型態\n\n.. _host:\n\nHOST\n-----------\n* 連線的 PTT 伺服器。\n\n.. py:attribute:: PyPtt.HOST.PTT1\n\n    批踢"
  },
  {
    "path": "make_doc.sh",
    "chars": 38,
    "preview": "make -C docs/ clean\nmake -C docs/ html"
  },
  {
    "path": "requirements.txt",
    "chars": 63,
    "preview": "progressbar2\nwebsockets\nuao\nrequests==2.31.0\nAutoStrEnum\nPyYAML"
  },
  {
    "path": "scripts/lang.py",
    "chars": 2459,
    "preview": "import json\nimport os\nimport sys\nfrom collections import defaultdict\n\nimport yaml\n\nsys.path.append(os.getcwd())\n\nimport "
  },
  {
    "path": "scripts/package_script.py",
    "chars": 2791,
    "preview": "import os\nimport subprocess\nimport time\n\n\ndef get_next_version():\n    is_merged = os.environ.get('GITHUB_EVENT_NAME') =="
  },
  {
    "path": "setup.py",
    "chars": 4441,
    "preview": "import os\nimport subprocess\nimport time\n\nfrom setuptools import setup\n\n\ndef version_automation_script():\n    is_merged ="
  },
  {
    "path": "tests/change_pw.py",
    "chars": 539,
    "preview": "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"
  },
  {
    "path": "tests/comment.py",
    "chars": 1919,
    "preview": "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_"
  },
  {
    "path": "tests/config.py",
    "chars": 142,
    "preview": "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 = "
  },
  {
    "path": "tests/exceptions.py",
    "chars": 207,
    "preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\nimport PyPtt\n\nif __name__ == '__main__':\n    try:\n        raise PyPtt"
  },
  {
    "path": "tests/get_board_info.py",
    "chars": 698,
    "preview": "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"
  },
  {
    "path": "tests/get_board_list.py",
    "chars": 686,
    "preview": "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:"
  },
  {
    "path": "tests/get_bottom_post_list.py",
    "chars": 570,
    "preview": "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"
  },
  {
    "path": "tests/get_favourite_boards.py",
    "chars": 583,
    "preview": "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"
  },
  {
    "path": "tests/get_mail.py",
    "chars": 724,
    "preview": "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"
  },
  {
    "path": "tests/get_newest_index.py",
    "chars": 2022,
    "preview": "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"
  },
  {
    "path": "tests/get_post.py",
    "chars": 4787,
    "preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport json\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import ut"
  },
  {
    "path": "tests/get_time.py",
    "chars": 672,
    "preview": "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"
  },
  {
    "path": "tests/get_user.py",
    "chars": 708,
    "preview": "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: "
  },
  {
    "path": "tests/give_p.py",
    "chars": 542,
    "preview": "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"
  },
  {
    "path": "tests/i18n.py",
    "chars": 360,
    "preview": "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"
  },
  {
    "path": "tests/init.py",
    "chars": 1602,
    "preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\n\n\ndef test():\n    print('=== default ===')\n    PyPtt.AP"
  },
  {
    "path": "tests/logger.py",
    "chars": 529,
    "preview": "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"
  },
  {
    "path": "tests/login_logout.py",
    "chars": 727,
    "preview": "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: "
  },
  {
    "path": "tests/performance.py",
    "chars": 667,
    "preview": "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):"
  },
  {
    "path": "tests/post.py",
    "chars": 3558,
    "preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport time\n\nimport PyPtt\n\nfrom PyPtt import PostField\nfrom tests im"
  },
  {
    "path": "tests/reply.py",
    "chars": 2666,
    "preview": "import os\nimport sys\nimport time\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom PyPtt import Pos"
  },
  {
    "path": "tests/search_user.py",
    "chars": 690,
    "preview": "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"
  },
  {
    "path": "tests/service.py",
    "chars": 1167,
    "preview": "import os\nimport sys\nimport threading\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import Service\nfrom tests i"
  },
  {
    "path": "tests/util.py",
    "chars": 1842,
    "preview": "import json\n\nimport PyPtt\nfrom PyPtt import PostField\nfrom PyPtt import log\nfrom . import config\n\n\ndef log_to_file(msg: "
  },
  {
    "path": "upload.sh",
    "chars": 355,
    "preview": "#!/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\nrele"
  }
]

About this extraction

This page contains the full source code of the PttCodingMan/PyPtt GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 119 files (328.8 KB), approximately 106.0k tokens, and a symbol index with 299 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!