Repository: PttCodingMan/PyPtt
Branch: master
Commit: f277f1adf595
Files: 119
Total size: 328.8 KB
Directory structure:
gitextract_7t96yf9r/
├── .github/
│ └── workflows/
│ ├── deploy.yml
│ ├── docs.yml
│ └── test.yml
├── .gitignore
├── GourceScript.bat
├── LICENSE
├── MANIFEST.in
├── PyPtt/
│ ├── PTT.py
│ ├── __init__.py
│ ├── _api_bucket.py
│ ├── _api_call_status.py
│ ├── _api_change_pw.py
│ ├── _api_comment.py
│ ├── _api_del_post.py
│ ├── _api_get_board_info.py
│ ├── _api_get_board_list.py
│ ├── _api_get_bottom_post_list.py
│ ├── _api_get_favourite_board.py
│ ├── _api_get_newest_index.py
│ ├── _api_get_post.py
│ ├── _api_get_post_index.py
│ ├── _api_get_time.py
│ ├── _api_get_user.py
│ ├── _api_give_money.py
│ ├── _api_has_new_mail.py
│ ├── _api_loginout.py
│ ├── _api_mail.py
│ ├── _api_mark_post.py
│ ├── _api_post.py
│ ├── _api_reply_post.py
│ ├── _api_search_user.py
│ ├── _api_set_board_title.py
│ ├── _api_util.py
│ ├── check_value.py
│ ├── command.py
│ ├── config.py
│ ├── connect_core.py
│ ├── data_type.py
│ ├── exceptions.py
│ ├── i18n.py
│ ├── lang/
│ │ ├── en_US.yaml
│ │ └── zh_TW.yaml
│ ├── lib_util.py
│ ├── log.py
│ ├── screens.py
│ └── service.py
├── README.md
├── docs/
│ ├── CNAME
│ ├── Makefile
│ ├── api/
│ │ ├── bucket.rst
│ │ ├── change_pw.rst
│ │ ├── comment.rst
│ │ ├── del_mail.rst
│ │ ├── del_post.rst
│ │ ├── get_aid_from_url.rst
│ │ ├── get_all_boards.rst
│ │ ├── get_board_info.rst
│ │ ├── get_bottom_post_list.rst
│ │ ├── get_favourite_boards.rst
│ │ ├── get_mail.rst
│ │ ├── get_newest_index.rst
│ │ ├── get_post.rst
│ │ ├── get_time.rst
│ │ ├── get_user.rst
│ │ ├── give_money.rst
│ │ ├── index.rst
│ │ ├── init.rst
│ │ ├── login_logout.rst
│ │ ├── mail.rst
│ │ ├── mark_post.rst
│ │ ├── post.rst
│ │ ├── reply_post.rst
│ │ ├── search_user.rst
│ │ └── set_board_title.rst
│ ├── changelog.rst
│ ├── conf.py
│ ├── dev.rst
│ ├── docker.rst
│ ├── examples.rst
│ ├── exceptions.rst
│ ├── faq.rst
│ ├── index.rst
│ ├── install.rst
│ ├── make.bat
│ ├── requirements.txt
│ ├── roadmap.rst
│ ├── robots.txt
│ ├── service.rst
│ └── type.rst
├── make_doc.sh
├── requirements.txt
├── scripts/
│ ├── lang.py
│ └── package_script.py
├── setup.py
├── tests/
│ ├── change_pw.py
│ ├── comment.py
│ ├── config.py
│ ├── exceptions.py
│ ├── get_board_info.py
│ ├── get_board_list.py
│ ├── get_bottom_post_list.py
│ ├── get_favourite_boards.py
│ ├── get_mail.py
│ ├── get_newest_index.py
│ ├── get_post.py
│ ├── get_time.py
│ ├── get_user.py
│ ├── give_p.py
│ ├── i18n.py
│ ├── init.py
│ ├── logger.py
│ ├── login_logout.py
│ ├── performance.py
│ ├── post.py
│ ├── reply.py
│ ├── search_user.py
│ ├── service.py
│ └── util.py
└── upload.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/deploy.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: deploy
# run on merge to master or manual trigger
on:
pull_request:
types: [ closed ]
branches:
- master
paths:
- 'PyPtt/*.py'
- 'setup.py'
workflow_dispatch:
jobs:
deploy:
name: Deploy to PyPI and Docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: |
python -m build
- name: Publish package to TestPyPI
if: github.event_name == 'workflow_dispatch' && github.event.pull_request.merged == false
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
- name: Publish package
if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
- name: Trigger Docker build
if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true
uses: InformaticsMatters/trigger-ci-action@1.0.1
with:
ci-owner: PyPtt
ci-repository: PyPtt_image
ci-user: PttCodingMan
ci-ref: refs/heads/main
ci-user-token: ${{ secrets.ACCESS_TOKEN }}
ci-name: build PyPtt image
================================================
FILE: .github/workflows/docs.yml
================================================
name: docs
# run on merge to master or manual trigger
on:
pull_request:
types: [ closed ]
paths:
- 'docs/**/*'
- 'PyPtt/*.py'
workflow_dispatch:
jobs:
build:
if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true
name: Build doc and Deploy
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
- uses: actions/checkout@v4
with:
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
- name: Build and Commit
uses: sphinx-notes/pages@v2
with:
requirements_path: docs/requirements.txt
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages
================================================
FILE: .github/workflows/test.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: test
# run on every PR creation, or manual trigger
# run on every push to non-master branch
on:
pull_request:
types:
- opened
push:
branches-ignore:
- 'master'
workflow_dispatch:
env:
DEP_PATH: requirements.txt
jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 PyPtt/ --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 PyPtt/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test
run: |
python tests/init.py
scan:
runs-on: ubuntu-latest
steps:
- name: Check out master
uses: actions/checkout@v4
- name: detect-secrets
uses: reviewdog/action-detect-secrets@master
with:
reporter: github-pr-review
- name: Security vulnerabilities scan
uses: aufdenpunkt/python-safety-check@master
with:
scan_requirements_file_only: true
================================================
FILE: .gitignore
================================================
__pycache__/
build/
dist/
/CrawlBoardResult.txt
/.pypirc
PTTLibrary.egg-info/
*Out.txt
/Big5Data.txt
PTTLibrary-*/
/PTTLibrary/i18n.txt
/Test.txt
Account*.txt
*.spec
/LogHandler.txt
.vscode
.idea/
venv/
/log.txt
PyPtt.egg-info/
/test_account*.txt
/test_result.txt
*.json
tests/ptt.sh
docs/_build/
.DS_Store
test*.py
ptt.sh
================================================
FILE: GourceScript.bat
================================================
@echo off
cls
gource --seconds-per-day 0.05 --title "PTT Library"
================================================
FILE: LICENSE
================================================
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
================================================
FILE: MANIFEST.in
================================================
include LICENSE
include README.md
# Include the data files
recursive-include PyPtt *
================================================
FILE: PyPtt/PTT.py
================================================
from __future__ import annotations
import functools
import threading
from typing import Dict, Tuple, Callable, List, Optional, Any
from . import __version__
from . import _api_bucket
from . import _api_change_pw
from . import _api_comment
from . import _api_del_post
from . import _api_get_board_info
from . import _api_get_board_list
from . import _api_get_bottom_post_list
from . import _api_get_favourite_board
from . import _api_get_newest_index
from . import _api_get_post
from . import _api_get_time
from . import _api_get_user
from . import _api_give_money
from . import _api_loginout
from . import _api_mail
from . import _api_mark_post
from . import _api_post
from . import _api_reply_post
from . import _api_search_user
from . import _api_set_board_title
from . import check_value
from . import config
from . import connect_core
from . import data_type
from . import i18n
from . import lib_util
from . import log
class API:
def __init__(self, **kwargs):
"""
初始化 PyPtt。
Args:
language (:ref:`language`): PyPtt 顯示訊息的語言。預設為 **MANDARIN**。
log_level (LogLevel_): PyPtt 顯示訊息的等級。預設為 **INFO**。
screen_timeout (int): 經過 screen_timeout 秒之後, PyPtt 將會判定無法判斷目前畫面的狀況。預設為 **3 秒**。
screen_long_timeout (int): 經過 screen_long_timeout 秒之後,PyPtt 將會判定無法判斷目前畫面的狀況,這會用在較長的等待時間,例如踢掉其他連線等等。預設為 **10 秒**。
screen_post_timeout (int): 經過 screen_post_timeout 秒之後,PyPtt 將會判定無法判斷目前畫面的狀況,這會用在較長的等待時間,例如發佈文章等等。預設為 **60 秒**。
connect_mode (:ref:`connect-mode`): PyPtt 連線的模式。預設為 **WEBSOCKETS**。
logger_callback (Callable): PyPtt 顯示訊息的 callback。預設為 None。
port (int): PyPtt 連線的 port。預設為 **23**。
host (:ref:`host`): PyPtt 連線的 PTT 伺服器。預設為 **PTT1**。
check_update (bool): 是否檢查 PyPtt 的更新。預設為 **True**。
Returns:
None
範例::
import PyPtt
ptt_bot = PyPtt.API()
參考: :ref:`language`、LogLevel_、:ref:`connect-mode`、:ref:`host`
.. _LogLevel: https://github.com/PttCodingMan/SingleLog/blob/d7c19a1b848dfb1c9df8201f13def9a31afd035c/SingleLog/SingleLog.py#L22
英文顯示範例::
import PyPtt
ptt_bot = PyPtt.API(
language=PyPtt.Language.ENGLISH)
除錯範例::
import PyPtt
ptt_bot = PyPtt.API(
log_level=PyPtt.LogLevel.DEBUG)
"""
log_level = kwargs.get('log_level', log.INFO)
if not isinstance(log_level, log.LogLv):
raise TypeError('[PyPtt] log_level must be log.Level')
logger_callback = kwargs.get('logger_callback', None)
log.init(log_level, logger_callback=logger_callback)
language = kwargs.get('language', data_type.Language.MANDARIN)
if not isinstance(language, str):
raise TypeError('[PyPtt] language must be PyPtt.Language')
if language not in i18n.locale_pool:
raise TypeError('[PyPtt] language must be PyPtt.Language')
self.config = config.Config()
self.config.log_level = log_level
self.config.language = language
print('language', self.config.language)
i18n.init(self.config.language)
self.is_mailbox_full: bool = False
self.is_registered_user: bool = False
self.process_picks: int = 0
self.ptt_id: str = ''
self._ptt_pw: str = ''
self._is_login: bool = False
host = kwargs.get('host', data_type.HOST.PTT1)
screen_timeout = kwargs.get('screen_timeout', 3.0)
screen_long_timeout = kwargs.get('screen_long_timeout', 10.0)
screen_post_timeout = kwargs.get('screen_post_timeout', 60.0)
check_value.check_type(host, (data_type.HOST, str), 'host')
check_value.check_type(screen_timeout, float, 'screen_timeout')
check_value.check_type(screen_long_timeout, float, 'screen_long_timeout')
check_value.check_type(screen_post_timeout, float, 'screen_post_timeout')
if screen_timeout != 0:
self.config.screen_timeout = screen_timeout
if screen_long_timeout != 0:
self.config.screen_long_timeout = screen_long_timeout
if screen_post_timeout != 0:
self.config.screen_post_timeout = screen_post_timeout
self.config.host = host
self.host = host
port = kwargs.get('port', 23)
check_value.check_type(port, int, 'port')
check_value.check_range(port, 1, 65535 - 1, 'port')
self.config.port = port
connect_mode = kwargs.get('connect_mode', data_type.ConnectMode.WEBSOCKETS)
check_value.check_type(connect_mode, data_type.ConnectMode, 'connect_mode')
if host in [data_type.HOST.PTT1, data_type.HOST.PTT2] and connect_mode is data_type.ConnectMode.TELNET:
raise ValueError('[PyPtt] TELNET is not available on PTT1 and PTT2')
self.config.connect_mode = connect_mode
self.connect_core = connect_core.API(self.config)
self._exist_board_list = []
self._moderators = dict()
self._thread_id = threading.get_ident()
self._goto_board_list = []
self._board_info_list = dict()
self._newest_index_data = data_type.TimedDict(timeout=2)
log.logger.debug('thread_id', self._thread_id)
log.logger.info(
i18n.replace(i18n.welcome, __version__))
log.logger.info('PyPtt', i18n.initialization)
if self.config.connect_mode == data_type.ConnectMode.TELNET:
log.logger.info(i18n.set_connect_mode, '...', i18n.connect_mode_TELNET)
elif self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:
log.logger.info(i18n.set_connect_mode, '...', i18n.connect_mode_WEBSOCKET)
if self.config.language == data_type.Language.MANDARIN:
log.logger.info(i18n.set_up_lang_module, '...', i18n.mandarin_module)
elif self.config.language == data_type.Language.ENGLISH:
log.logger.info(i18n.set_up_lang_module, '...', i18n.english_module)
if self.config.host == data_type.HOST.PTT1:
log.logger.info(i18n.set_connect_host, '...', i18n.PTT)
elif self.config.host == data_type.HOST.PTT2:
log.logger.info(i18n.set_connect_host, '...', i18n.PTT2)
elif self.config.host == data_type.HOST.LOCALHOST:
log.logger.info(i18n.set_connect_host, '...', i18n.localhost)
else:
log.logger.info(i18n.set_connect_host, '...', self.config.host)
log.logger.info('PyPtt', i18n.initialization, '...', i18n.done)
check_update = kwargs.get('check_update', True)
check_value.check_type(check_update, bool, 'check_update')
if check_update:
version_compare, remote_version = lib_util.sync_version()
if version_compare is data_type.Compare.SMALLER:
log.logger.info(i18n.current_version, __version__)
log.logger.info(i18n.new_version, remote_version)
elif version_compare is data_type.Compare.BIGGER:
log.logger.info(i18n.development_version, __version__)
else:
log.logger.info(i18n.latest_version, __version__)
else:
log.logger.info(i18n.current_version, __version__)
def __del__(self):
if log.logger:
log.logger.debug(i18n.goodbye)
def login(self, ptt_id: str, ptt_pw: str, kick_other_session: bool = False) -> None:
"""
登入 PTT。
Args:
ptt_id (str): PTT ID。
ptt_pw (str): PTT 密碼。
kick_other_session (bool): 是否踢掉其他登入的 session。預設為 False。
Returns:
None
Raises:
LoginError: 登入失敗。
WrongIDorPassword: 帳號或密碼錯誤。
OnlySecureConnection: 只能使用安全連線。
ResetYourContactEmail: 請先至信箱設定連絡信箱。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
ptt_bot.login(
ptt_id='ptt_id', ptt_pw='ptt_pw', kick_other_session=True)
except PyPtt.LoginError:
print('登入失敗')
except PyPtt.WrongIDorPassword:
print('帳號密碼錯誤')
except PyPtt.OnlySecureConnection:
print('只能使用安全連線')
except PyPtt.ResetYourContactEmail:
print('請先至信箱設定連絡信箱')
"""
_api_loginout.login(self, ptt_id, ptt_pw, kick_other_session)
def logout(self) -> None:
"""
登出 PTT。
Returns:
None
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_loginout.logout(self)
def get_time(self) -> str:
"""
取得 PTT 系統時間。
Returns:
None
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
time = ptt_bot.get_time()
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_get_time.get_time(self)
def get_post(self, board: str, aid: Optional[str] = None, index: Optional[int] = None,
search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None,
search_list: Optional[List[tuple]] = None, query: bool = False) -> Dict:
"""
取得文章。
Args:
board (str): 看板名稱。
aid (str): 文章編號。
index: 文章編號。
search_list (List[str]): 搜尋清單。
query (bool): 是否為查詢模式。
Returns:
Dict,文章內容。詳見 :ref:`post-field`
Raises:
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
使用 AID 範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
post_info = ptt_bot.get_post('Python', aid='1TJH_XY0')
# .. do something ..
finally:
ptt_bot.logout()
使用 index 範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
post_info = ptt_bot.get_post('Python', index=1)
# .. do something ..
finally:
ptt_bot.logout()
使用搜尋範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
post_info = ptt_bot.get_post(
'Python',
index=1,
search_list=[(PyPtt.SearchType.KEYWORD, 'PyPtt')]
)
# .. do something ..
finally:
ptt_bot.logout()
| 更多範例參考 :ref:`取得文章 `
| 參考 :ref:`取得最新文章編號 `
"""
return _api_get_post.get_post(
self, board, aid=aid, index=index, search_type=search_type, search_condition=search_condition,
search_list=search_list, query=query)
def get_newest_index(self, index_type: data_type.NewIndex, board: Optional[str] = None,
search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None,
search_list: Optional[List[Tuple[Any | str]]] = None, ) -> int:
"""
取得最新文章或信箱編號。
Args:
index_type (:ref:`new-index`): 編號類型。
board (str): 看板名稱。
search_list (List[str]): 搜尋清單。
Returns:
int,最新文章或信箱編號。
Raises:
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
取得最新看板編號::
import PyPtt
ptt_bot = PyPtt.API()
# get newest index of board
try:
# .. login ..
newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, 'Python')
# .. do something ..
finally:
ptt_bot.logout()
取得最新文章編號使用搜尋::
import PyPtt
ptt_bot = PyPtt.API()
search_list = [(PyPtt.SearchType.KEYWORD, 'PyPtt')]
try:
# .. login ..
newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, 'Python', search_list=search_list)
# .. do something ..
finally:
ptt_bot.logout()
取得最新信箱編號::
import PyPtt
ptt_bot = PyPtt.API()
# get newest index of mail
try:
# .. login ..
newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.MAIL)
# .. do something ..
finally:
ptt_bot.logout()
參考 :ref:`搜尋編號種類 `、:ref:`取得文章 `
"""
return _api_get_newest_index.get_newest_index(
self, index_type, board, search_type, search_condition, search_list)
def post(self, board: str, title_index: int, title: str, content: str, sign_file: [str | int] = 0) -> None:
"""
發文。
Args:
board (str): 看板名稱。
title_index (int): 文章標題編號。
title (str): 文章標題。
content (str): 文章內容。
sign_file (str | int): 編號或隨機簽名檔 (x),預設為 0 (不選)。
Returns:
None
Raises:
UnregisteredUser: 未註冊使用者。
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
NoPermission: 沒有發佈權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content='測試內容', sign_file=0)
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_post.post(self, board, title, content, title_index, sign_file)
def comment(self, board: str, comment_type: data_type.CommentType, content: str, aid: Optional[str] = None,
index: int = 0) -> None:
"""
推文。
Args:
board (str): 看板名稱。
comment_type (:ref:`comment-type`): 推文類型。
content (str): 推文內容。
aid (str): 文章編號。
index (int): 文章編號。
Returns:
None
Raises:
UnregisteredUser: 未註冊使用者。
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
NoSuchPost: 文章不存在。
NoPermission: 沒有推文權限。
NoFastComment: 推文間隔太短。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.PUSH, content='Comment by index', index=123)
ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.PUSH, content='Comment by index', aid='17MrayxF')
# .. do something ..
finally:
ptt_bot.logout()
參考 :ref:`推文類型 `、:ref:`取得最新文章編號 `
"""
_api_comment.comment(self, board, comment_type, content, aid, index)
def get_user(self, user_id: str) -> Dict:
"""
取得使用者資訊。
Args:
user_id (str): 使用者 ID。
Returns:
Dict,使用者資訊。詳見 :ref:`使用者資料欄位 `
Raises:
RequireLogin: 需要登入。
NoSuchUser: 使用者不存在。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
user_info = ptt_bot.get_user('CodingMan')
# .. do something ..
finally:
ptt_bot.logout()
參考 :ref:`使用者資料欄位 `
"""
return _api_get_user.get_user(self, user_id)
def give_money(self, ptt_id: str, money: int, red_bag_title: Optional[str] = None,
red_bag_content: Optional[str] = None) -> None:
"""
轉帳,詳見 `P 幣`_。
.. _`P 幣`: https://pttpedia.fandom.com/zh/wiki/P%E5%B9%A3
Args:
ptt_id (str): PTT ID。
money (int): 轉帳金額。
red_bag_title (str): 紅包標題。
red_bag_content (str): 紅包內容。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchUser: 使用者不存在。
NoMoney: 餘額不足。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.give_money(ptt_id='CodingMan', money=100)
# or
ptt_bot.give_money('CodingMan', 100, red_bag_title='紅包袋標題', red_bag_content='紅包袋內文')
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_give_money.give_money(self, ptt_id, money, red_bag_title, red_bag_content)
def mail(self, ptt_id: str, title: str, content: str, sign_file: [int | str] = 0,
backup: bool = True) -> None:
"""
寄信。
Args:
ptt_id (str): PTT ID。
title (str): 信件標題。
content (str): 信件內容。
sign_file (str | int): 編號或隨機簽名檔 (x),預設為 0 (不選)。
backup (bool): 如果是 True 寄信時將會備份信件,預設為 True。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchUser: 使用者不存在。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.mail(ptt_id='CodingMan', title='信件標題', content='信件內容')
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_mail.mail(self, ptt_id, title, content, sign_file, backup)
def get_all_boards(self) -> List[str]:
"""
取得全站看板清單。
Returns:
List[str],看板清單。
Raises:
RequireLogin: 需要登入。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
board_list = ptt_bot.get_all_boards()
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_get_board_list.get_board_list(self)
def reply_post(self, reply_to: data_type.ReplyTo, board: str, content: str, sign_file: [str | int] = 0,
aid: Optional[str] = None, index: int = 0) -> None:
"""
回覆文章。
Args:
reply_to (:ref:`reply-to`): 回覆類型。
board (str): 看板名稱。
content (str): 回覆內容。
sign_file (str | int): 編號或隨機簽名檔 (x),預設為 **0** (不選)。
aid: 文章編號。
index: 文章編號。
Returns:
None
Raises:
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
NoSuchPost: 文章不存在。
NoPermission: 沒有回覆權限。
CantResponse: 已結案並標記, 不得回應。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.reply_post(reply_to=PyPtt.ReplyTo.BOARD, board='Test', content='PyPtt 程式回覆測試', index=123)
# .. do something ..
finally:
ptt_bot.logout()
參考 :ref:`回覆類型 `、:ref:`取得最新文章編號 `
"""
_api_reply_post.reply_post(self, reply_to, board, content, sign_file, aid, index)
def set_board_title(self, board: str, new_title: str) -> None:
"""
設定看板標題。
Args:
board (str): 看板名稱。
new_title (str): 新標題。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchBoard: 看板不存在。
NeedModeratorPermission: 需要看板管理員權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.set_board_title(board='Test', new_title='現在時間 %s' % datetime.datetime.now())
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_set_board_title.set_board_title(self, board, new_title)
def mark_post(self, mark_type: int, board: str, aid: Optional[str] = None, index: int = 0, search_type: int = 0,
search_condition: Optional[str] = None) -> None:
"""
標記文章。
Args:
mark_type (:ref:`mark-type`): 標記類型。
board (str): 看板名稱。
aid (str): 文章編號。
index (int): 文章編號。
search_type (:ref:`search-type`): 搜尋類型。
search_condition (str): 搜尋條件。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchBoard: 看板不存在。
NoSuchPost: 文章不存在。
NeedModeratorPermission: 需要看板管理員權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.mark_post(mark_type=PyPtt.MarkType.M, board='Test', index=123)
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_mark_post.mark_post(self, mark_type, board, aid, index, search_type, search_condition)
def get_favourite_boards(self) -> List[dict]:
"""
取得我的最愛清單。
Returns:
List[dict],收藏看板清單,詳見 :ref:`favorite-board-field`。
Raises:
RequireLogin: 需要登入。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
favourite_boards = ptt_bot.get_favourite_boards()
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_get_favourite_board.get_favourite_board(self)
def bucket(self, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:
"""
水桶。
Args:
board (str): 看板名稱。
bucket_days (int): 水桶天數。
reason (str): 水桶原因。
ptt_id (str): PTT ID。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchBoard: 看板不存在。
NoSuchUser: 使用者不存在。
NeedModeratorPermission: 需要看板管理員權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.bucket(board='Test', bucket_days=7, reason='PyPtt 程式水桶測試', ptt_id='test')
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_bucket.bucket(self, board, bucket_days, reason, ptt_id)
def search_user(self, ptt_id: str, min_page: Optional[int] = None, max_page: Optional[int] = None) -> List[str]:
"""
搜尋使用者。
Args:
ptt_id (str): PTT ID。
min_page (int): 最小頁數。
max_page (int): 最大頁數。
Returns:
List[str],搜尋結果。
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
search_result = ptt_bot.search_user(ptt_id='Coding')
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_search_user.search_user(self, ptt_id, min_page, max_page)
def get_board_info(self, board: str, get_post_types: bool = False) -> Dict:
"""
取得看板資訊。
Args:
board (str): 看板名稱。
get_post_types (bool): 是否取得文章類型,例如:八卦板的「問卦」。
Returns:
Dict,看板資訊,詳見 :ref:`board-field`。
Raises:
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
NoPermission: 沒有權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
board_info = ptt_bot.get_board_info(board='Test')
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_get_board_info.get_board_info(self, board, get_post_types, call_by_others=False)
def get_mail(self, index: int, search_type: Optional[data_type.SearchType] = None,
search_condition: Optional[str] = None,
search_list: Optional[list] = None) -> Dict:
"""
取得信件。
Args:
index (int): 信件編號。
search_type (:ref:`search-type`): 搜尋類型。
search_condition: 搜尋條件。
search_list: 搜尋清單。
Returns:
Dict,信件資訊,詳見 :ref:`mail-field`。
Raises:
RequireLogin: 需要登入。
NoSuchMail: 信件不存在。
NoPermission: 沒有權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
mail = ptt_bot.get_mail(index=1)
# .. do something ..
finally:
ptt_bot.logout()
參考 :doc:`get_newest_index`
"""
return _api_mail.get_mail(self, index, search_type, search_condition, search_list)
def del_mail(self, index: int) -> None:
"""
刪除信件。
Args:
index (int): 信件編號。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
MailboxFull: 信箱已滿。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.del_mail(index=1)
# .. do something ..
finally:
ptt_bot.logout()
參考 :doc:`get_newest_index`
"""
_api_mail.del_mail(self, index)
def change_pw(self, new_password: str) -> None:
"""
更改密碼。
備註:因批踢踢系統限制,最長密碼為 8 碼。
Args:
new_password (str): 新密碼。
Returns:
None
Raises:
RequireLogin: 需要登入。
SetContactMailFirst: 需要先設定聯絡信箱。
WrongPassword: 密碼錯誤。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.change_pw(new_password='123456')
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_change_pw.change_pw(self, new_password)
@functools.lru_cache(maxsize=64)
def get_aid_from_url(self, url: str) -> Tuple[str, str]:
"""
從網址取得看板名稱與文章編號。
Args:
url: 網址。
Returns:
Tuple[str, str],看板名稱與文章編號。
範例::
import PyPtt
ptt_bot = PyPtt.API()
url = 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html'
board, aid = ptt_bot.get_aid_from_url(url)
"""
return lib_util.get_aid_from_url(url)
def get_bottom_post_list(self, board: str) -> List[str]:
"""
取得看板置底文章清單。
Args:
board (str): 看板名稱。
Returns:
List[post],置底文章清單,詳見 :ref:`post-field`。
Raises:
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
bottom_post_list = ptt_bot.get_bottom_post_list(board='Python')
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_get_bottom_post_list.get_bottom_post_list(self, board)
def del_post(self, board: str, aid: Optional[str] = None, index: int = 0) -> None:
"""
刪除文章。
Args:
board (str): 看板名稱。
aid (str): 文章編號。
index (int): 文章編號。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchBoard: 看板不存在。
NoSuchPost: 文章不存在。
NoPermission: 沒有權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.del_post(board='Python', aid='1TJH_XY0')
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_del_post.del_post(self, board, aid, index)
def fast_post_step0(self, board: str, title: str, content: str, post_type: int) -> None:
_api_post.fast_post_step0(self, board, title, content, post_type)
def fast_post_step1(self, sign_file):
_api_post.fast_post_step1(self, sign_file)
if __name__ == '__main__':
print('PyPtt v ' + __version__)
print('Maintained by CodingMan')
================================================
FILE: PyPtt/__init__.py
================================================
__version__ = '1.1.2'
from .PTT import API
from .data_type import *
from .exceptions import *
from .log import LogLevel
from .service import Service
LOG_LEVEL = LogLevel
_main_version = '1.2'
================================================
FILE: PyPtt/_api_bucket.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import screens
def bucket(api, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(board, str, 'board')
check_value.check_type(bucket_days, int, 'bucket_days')
check_value.check_type(reason, str, 'reason')
check_value.check_type(ptt_id, str, 'ptt_id')
api.get_user(ptt_id)
_api_util.check_board(api, board, check_moderator=True)
_api_util.goto_board(api, board)
cmd_list = []
cmd_list.append('i')
cmd_list.append(command.ctrl_p)
cmd_list.append('w')
cmd_list.append(command.enter)
cmd_list.append('a')
cmd_list.append(command.enter)
cmd_list.append(ptt_id)
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
cmd_list = []
cmd_list.append(str(bucket_days))
cmd_list.append(command.enter)
cmd_list.append(reason)
cmd_list.append(command.enter)
cmd_list.append('y')
cmd_list.append(command.enter)
cmd_part2 = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('◆ 使用者之前已被禁言', exceptions_=exceptions.UserHasPreviouslyBeenBanned()),
connect_core.TargetUnit('請以數字跟單位(預設為天)輸入期限', response=cmd_part2),
connect_core.TargetUnit('其它鍵結束', response=command.enter),
connect_core.TargetUnit('權限設定系統', response=command.enter),
connect_core.TargetUnit('任意鍵', response=command.space),
connect_core.TargetUnit(screens.Target.InBoard, break_detect=True),
]
api.connect_core.send(
cmd,
target_list)
================================================
FILE: PyPtt/_api_call_status.py
================================================
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import log
from . import screens
def get_call_status(api) -> None:
# log.py = DefaultLogger('api', api.config.log_level)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('A')
cmd_list.append(command.right)
cmd_list.append(command.left)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('[呼叫器]打開', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('[呼叫器]拔掉', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('[呼叫器]防水', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('[呼叫器]好友', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('[呼叫器]關閉', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('★', log_level=log.DEBUG, response=cmd),
]
for i in range(2):
index = api.connect_core.send(cmd, target_list)
if index < 0:
if i == 0:
continue
raise exceptions.UnknownError('UnknownError')
if index == 0:
return data_type.call_status.ON
if index == 1:
return data_type.call_status.UNPLUG
if index == 2:
return data_type.call_status.WATERPROOF
if index == 3:
return data_type.call_status.FRIEND
if index == 4:
return data_type.call_status.OFF
ori_screen = api.connect_core.get_screen_queue()[-1]
raise exceptions.UnknownError(ori_screen)
def set_call_status(api, call_status) -> None:
# 打開 -> 拔掉 -> 防水 -> 好友 -> 關閉
current_call_status = api._get_call_status()
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append(command.ctrl_u)
cmd_list.append('p')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InUserList, break_detect=True)]
while current_call_status != call_status:
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
current_call_status = api._get_call_status()
================================================
FILE: PyPtt/_api_change_pw.py
================================================
from . import command, _api_util
from . import connect_core
from . import exceptions
from . import i18n
from . import log
def change_pw(api, new_password: str) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
log.logger.info(i18n.change_pw)
new_password = new_password[:8]
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('U')
cmd_list.append(command.enter)
cmd_list.append('I')
cmd_list.append(command.enter)
cmd_list.append('2')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('設定聯絡信箱後才能修改密碼', exceptions_=exceptions.SetContactMailFirst()),
connect_core.TargetUnit('您輸入的密碼不正確', exceptions_=exceptions.WrongPassword()),
connect_core.TargetUnit('請您確定(Y/N)?', response='Y' + command.enter),
connect_core.TargetUnit('檢查新密碼', response=new_password + command.enter, max_match=1),
connect_core.TargetUnit('設定新密碼', response=new_password + command.enter, max_match=1),
connect_core.TargetUnit('輸入原密碼', response=api._ptt_pw + command.enter, max_match=1),
connect_core.TargetUnit('設定個人資料與密碼', break_detect=True)
]
index = api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
if index < 0:
ori_screen = api.connect_core.get_screen_queue()[-1]
raise exceptions.UnknownError(ori_screen)
api._ptt_pw = new_password
log.logger.info(i18n.change_pw, '...', i18n.success)
================================================
FILE: PyPtt/_api_comment.py
================================================
import collections
import time
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
comment_option = [
None,
data_type.CommentType.PUSH,
data_type.CommentType.BOO,
data_type.CommentType.ARROW,
]
def _comment(api,
board: str,
push_type: data_type.CommentType,
push_content: str,
post_aid: str,
post_index: int) -> None:
_api_util.goto_board(api, board)
cmd_list = []
if post_aid is not None:
cmd_list.append(lib_util.check_aid(post_aid))
elif post_index != 0:
cmd_list.append(str(post_index))
else:
raise ValueError('post_aid and post_index cannot be None at the same time')
cmd_list.append(command.enter)
cmd_list.append(command.comment)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('您覺得這篇', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(f'→ {api.ptt_id}: ', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('加註方式', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('禁止快速連續推文', log_level=log.INFO, break_detect=True,
exceptions_=exceptions.NoFastComment()),
connect_core.TargetUnit('禁止短時間內大量推文', log_level=log.INFO, break_detect=True,
exceptions_=exceptions.NoFastComment()),
connect_core.TargetUnit('使用者不可發言', log_level=log.INFO, break_detect=True,
exceptions_=exceptions.NoPermission(i18n.no_permission)),
connect_core.TargetUnit('◆ 抱歉, 禁止推薦', log_level=log.INFO, break_detect=True,
exceptions_=exceptions.CantComment()),
]
index = api.connect_core.send(
cmd,
target_list)
if index == -1:
raise exceptions.UnknownError('unknown error in comment')
log.logger.debug(i18n.has_comment_permission)
cmd_list = []
if index == 0 or index == 1:
push_option_line = api.connect_core.get_screen_queue()[-1]
push_option_line = push_option_line.split('\n')[-1]
log.logger.debug('comment option line', push_option_line)
available_push_type = collections.defaultdict(lambda: False)
first_available_push_type = None
if '值得推薦' in push_option_line:
available_push_type[data_type.CommentType.PUSH] = True
if first_available_push_type is None:
first_available_push_type = data_type.CommentType.PUSH
if '只加→註解' in push_option_line:
available_push_type[data_type.CommentType.ARROW] = True
if first_available_push_type is None:
first_available_push_type = data_type.CommentType.ARROW
if '給它噓聲' in push_option_line:
available_push_type[data_type.CommentType.BOO] = True
if first_available_push_type is None:
first_available_push_type = data_type.CommentType.BOO
log.logger.debug('available_push_type', available_push_type)
if available_push_type[push_type] is False:
if first_available_push_type:
push_type = first_available_push_type
if True in available_push_type.values():
cmd_list.append(str(comment_option.index(push_type)))
cmd_list.append(push_content)
cmd_list.append(command.enter)
cmd_list.append('y')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
]
api.connect_core.send(
cmd,
target_list)
def comment(api, board: str, push_type: data_type.CommentType, push_content: str, post_aid: str,
post_index: int) -> None:
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
check_value.check_type(board, str, 'board')
if not isinstance(push_type, data_type.CommentType):
raise TypeError(f'CommentType must be data_type.CommentType')
check_value.check_type(push_content, str, 'push_content')
if post_aid is not None:
check_value.check_type(post_aid, str, 'aid')
check_value.check_type(post_index, int, 'index')
if len(board) == 0:
raise ValueError(f'wrong parameter board: {board}')
if post_index != 0 and isinstance(post_aid, str):
raise ValueError('wrong parameter index and aid can\'t both input')
if post_index == 0 and post_aid is None:
raise ValueError('wrong parameter index or aid must input')
if post_index != 0:
newest_index = api.get_newest_index(
data_type.NewIndex.BOARD,
board=board)
check_value.check_index('index', post_index, newest_index)
_api_util.check_board(api, board)
board_info = api._board_info_list[board.lower()]
if board_info[data_type.BoardField.is_comment_record_ip]:
log.logger.debug(i18n.record_ip)
if board_info[data_type.BoardField.is_comment_aligned]:
log.logger.debug(i18n.push_aligned)
max_push_length = 32
else:
log.logger.debug(i18n.not_push_aligned)
max_push_length = 43 - len(api.ptt_id)
else:
log.logger.debug(i18n.not_record_ip)
if board_info[data_type.BoardField.is_comment_aligned]:
log.logger.debug(i18n.push_aligned)
max_push_length = 46
else:
log.logger.debug(i18n.not_push_aligned)
max_push_length = 58 - len(api.ptt_id)
push_content = push_content.strip()
push_list = []
while push_content:
index = 0
jump = 0
while len(push_content[:index].encode('big5uao', 'replace')) < max_push_length:
if index == len(push_content):
break
if push_content[index] == '\n':
jump = 1
break
index += 1
push_list.append(push_content[:index])
push_content = push_content[index + jump:]
push_list = filter(None, push_list)
for comment in push_list:
log.logger.info(i18n.comment)
for _ in range(2):
try:
_comment(api, board, push_type, comment, post_aid=post_aid, post_index=post_index)
break
except exceptions.NoFastComment:
# screens.show(api.config, api.connect_core.getScreenQueue())
log.logger.info(i18n.wait_for_no_fast_comment)
time.sleep(5.2)
log.logger.info(i18n.comment, '...', i18n.success)
================================================
FILE: PyPtt/_api_del_post.py
================================================
from __future__ import annotations
from typing import Optional
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0) -> None:
_api_util.one_thread(api)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
check_value.check_type(board, str, 'board')
if post_aid is not None:
check_value.check_type(post_aid, str, 'PostAID')
check_value.check_type(post_index, int, 'PostIndex')
if len(board) == 0:
raise ValueError(f'board error parameter: {board}')
if post_index != 0 and isinstance(post_aid, str):
raise ValueError('wrong parameter index and aid can\'t both input')
if post_index == 0 and post_aid is None:
raise ValueError('wrong parameter index or aid must input')
if post_index != 0:
newest_index = api.get_newest_index(
data_type.NewIndex.BOARD,
board=board)
check_value.check_index(
'PostIndex',
post_index,
newest_index)
log.logger.info(i18n.delete_post)
board_info = _api_util.check_board(api, board)
check_author = True
for moderator in board_info[data_type.BoardField.moderators]:
if api.ptt_id.lower() == moderator.lower():
check_author = False
break
log.logger.info(i18n.delete_post)
post_info = api.get_post(board, aid=post_aid, index=post_index, query=True)
if post_info[data_type.PostField.post_status] != data_type.PostStatus.EXISTS:
# delete success
log.logger.info(i18n.success)
return
if check_author:
if api.ptt_id.lower() != post_info[data_type.PostField.author].lower():
log.logger.info(i18n.delete_post, '...', i18n.fail)
raise exceptions.NoPermission(i18n.no_permission)
_api_util.goto_board(api, board)
cmd_list = []
if post_aid is not None:
cmd_list.append(lib_util.check_aid(post_aid))
elif post_index != 0:
cmd_list.append(str(post_index))
else:
raise ValueError('post_aid and post_index cannot be None at the same time')
cmd_list.append(command.enter)
cmd_list.append('d')
cmd = ''.join(cmd_list)
api.confirm = False
def confirm_delete_handler(screen):
api.confirm = True
target_list = [
connect_core.TargetUnit('請按任意鍵繼續', response=' '),
connect_core.TargetUnit('請確定刪除(Y/N)?[N]', response='y' + command.enter, handler=confirm_delete_handler,
max_match=1),
connect_core.TargetUnit(screens.Target.InBoard, break_detect=True),
]
index = api.connect_core.send(
cmd,
target_list)
if index == 1:
if not api.confirm:
log.logger.info(i18n.delete_post, '...', i18n.fail)
raise exceptions.NoPermission(i18n.no_permission)
log.logger.info(i18n.delete_post, '...', i18n.success)
================================================
FILE: PyPtt/_api_get_board_info.py
================================================
import re
from typing import Dict
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens
from .data_type import BoardField
def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -> Dict:
logger = log.init(log.DEBUG if call_by_others else log.INFO)
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
check_value.check_type(board, str, 'board')
logger.info(
i18n.replace(i18n.get_board_info, board))
_api_util.goto_board(api, board, refresh=True)
ori_screen = api.connect_core.get_screen_queue()[-1]
# print(ori_screen)
nuser = None
for line in ori_screen.split('\n'):
if '編號' not in line:
continue
if '日 期' not in line:
continue
if '人氣' not in line:
continue
nuser = line
break
if nuser is None:
raise exceptions.NoSuchBoard(api.config, board)
# print('------------------------')
# print('nuser', nuser)
# print('------------------------')
if '[靜]' in nuser:
online_user = 0
else:
if '編號' not in nuser or '人氣' not in nuser:
raise exceptions.NoSuchBoard(api.config, board)
pattern = re.compile('[\d]+')
r = pattern.search(nuser)
if r is None:
raise exceptions.NoSuchBoard(api.config, board)
# 減一是把自己本身拿掉
online_user = int(r.group(0)) - 1
logger.debug('人氣', online_user)
target_list = [
connect_core.TargetUnit('任意鍵繼續', log_level=log.DEBUG if call_by_others else log.INFO,
break_detect=True),
]
api.connect_core.send(
'i',
target_list)
ori_screen = api.connect_core.get_screen_queue()[-1]
# print(ori_screen)
p = re.compile('《(.+)》看板設定')
r = p.search(ori_screen)
if r is not None:
boardname = r.group(0)[1:-5].strip()
logger.debug('看板名稱', boardname, board)
if boardname.lower() != board.lower():
raise exceptions.NoSuchBoard(api.config, board)
p = re.compile('中文敘述: (.+)')
r = p.search(ori_screen)
if r is not None:
chinese_des = r.group(0)[5:].strip()
logger.debug('中文敘述', chinese_des)
p = re.compile('板主名單: (.+)')
r = p.search(ori_screen)
if r is not None:
moderator_line = r.group(0)[5:].strip()
if '(無)' in moderator_line:
moderators = []
else:
moderators = moderator_line.split('/')
for moderator in moderators.copy():
if moderator == '徵求中':
moderators.remove(moderator)
logger.debug('板主名單', moderators)
open_status = ('公開狀態(是否隱形): 公開' in ori_screen)
logger.debug('公開狀態', open_status)
into_top_ten_when_hide = ('隱板時 可以 進入十大排行榜' in ori_screen)
logger.debug('隱板時可以進入十大排行榜', into_top_ten_when_hide)
non_board_members_post = ('開放 非看板會員發文' in ori_screen)
logger.debug('非看板會員發文', non_board_members_post)
reply_post = ('開放 回應文章' in ori_screen)
logger.debug('回應文章', reply_post)
self_del_post = ('開放 自刪文章' in ori_screen)
logger.debug('自刪文章', self_del_post)
push_post = ('開放 推薦文章' in ori_screen)
logger.debug('推薦文章', push_post)
boo_post = ('開放 噓文' in ori_screen)
logger.debug('噓文', boo_post)
# 限制 快速連推文章, 最低間隔時間: 5 秒
# 開放 快速連推文章
fast_push = ('開放 快速連推文章' in ori_screen)
logger.debug('快速連推文章', fast_push)
if not fast_push:
p = re.compile('最低間隔時間: [\d]+')
r = p.search(ori_screen)
if r is not None:
min_interval = r.group(0)[7:].strip()
min_interval = int(min_interval)
else:
min_interval = 0
logger.debug('最低間隔時間', min_interval)
else:
min_interval = 0
# 推文時 自動 記錄來源 IP
# 推文時 不會 記錄來源 IP
push_record_ip = ('推文時 自動 記錄來源 IP' in ori_screen)
logger.debug('記錄來源 IP', push_record_ip)
# 推文時 對齊 開頭
# 推文時 不用對齊 開頭
push_aligned = ('推文時 對齊 開頭' in ori_screen)
logger.debug('對齊開頭', push_aligned)
# 板主 可 刪除部份違規文字
moderator_can_del_illegal_content = ('板主 可 刪除部份違規文字' in ori_screen)
logger.debug('板主可刪除部份違規文字', moderator_can_del_illegal_content)
# 轉錄文章 會 自動記錄,且 需要 發文權限
tran_post_auto_recorded_and_require_post_permissions = ('轉錄文章 會 自動記錄,且 需要 發文權限' in ori_screen)
logger.debug('轉錄文章 會 自動記錄,且 需要 發文權限', tran_post_auto_recorded_and_require_post_permissions)
cool_mode = ('未 設為冷靜模式' not in ori_screen)
logger.debug('冷靜模式', cool_mode)
require18 = ('禁止 未滿十八歲進入' in ori_screen)
logger.debug('禁止未滿十八歲進入', require18)
p = re.compile('登入次數 [\d]+ 次以上')
r = p.search(ori_screen)
if r is not None:
require_login_time = r.group(0).split(' ')[1]
require_login_time = int(require_login_time)
else:
require_login_time = 0
logger.debug('發文限制登入次數', require_login_time)
p = re.compile('退文篇數 [\d]+ 篇以下')
r = p.search(ori_screen)
if r is not None:
require_illegal_post = r.group(0).split(' ')[1]
require_illegal_post = int(require_illegal_post)
else:
require_illegal_post = 0
logger.debug('發文限制退文篇數', require_illegal_post)
kind_list = []
if get_post_kind:
_api_util.goto_board(api, board)
# Go certain board, then post to get post type info
cmd_list = []
cmd_list.append(command.ctrl_p)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
connect_core.TargetUnit('或不選)', break_detect=True)
]
index = api.connect_core.send(
cmd,
target_list)
if index == 0:
raise exceptions.NoPermission(i18n.no_permission)
# no post permission
ori_screen = api.connect_core.get_screen_queue()[-1]
screen_lines = ori_screen.split('\n')
for i in screen_lines:
if '種類:' in i:
type_pattern = re.compile('\d\.([^\ ]*)')
# 0 is not present any type that the key hold None object
kind_list = type_pattern.findall(i)
break
# Clear post status
cmd_list = []
cmd_list.append(command.ctrl_c)
cmd_list.append(command.ctrl_c)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InBoard, break_detect=True)
]
api.connect_core.send(
cmd,
target_list)
logger.info(
i18n.replace(i18n.get_board_info, board),
'...', i18n.success
)
return {
BoardField.board: boardname,
BoardField.online_user: online_user,
BoardField.mandarin_des: chinese_des,
BoardField.moderators: moderators,
BoardField.open_status: open_status,
BoardField.into_top_ten_when_hide: into_top_ten_when_hide,
BoardField.can_non_board_members_post: non_board_members_post,
BoardField.can_reply_post: reply_post,
BoardField.self_del_post: self_del_post,
BoardField.can_comment_post: push_post,
BoardField.can_boo_post: boo_post,
BoardField.can_fast_push: fast_push,
BoardField.min_interval_between_comments: min_interval,
BoardField.is_comment_record_ip: push_record_ip,
BoardField.is_comment_aligned: push_aligned,
BoardField.can_moderators_del_illegal_content: moderator_can_del_illegal_content,
BoardField.does_tran_post_auto_recorded_and_require_post_permissions: tran_post_auto_recorded_and_require_post_permissions,
BoardField.is_cool_mode: cool_mode,
BoardField.is_require18: require18,
BoardField.require_login_time: require_login_time,
BoardField.require_illegal_post: require_illegal_post,
BoardField.post_kind_list: kind_list
}
================================================
FILE: PyPtt/_api_get_board_list.py
================================================
import progressbar
from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens
def get_board_list(api) -> list:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
cmd_list = [
command.go_main_menu,
'F',
command.enter,
'y',
'$']
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InBoardList, break_detect=True)
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
ori_screen = api.connect_core.get_screen_queue()[-1]
max_no = 0
for line in ori_screen.split('\n'):
if '◎' not in line and '●' not in line:
continue
if line.startswith(api.cursor):
line = line[len(api.cursor):]
# print(f'->{line}<')
if '◎' in line:
front_part = line[:line.find('◎')]
else:
front_part = line[:line.find('●')]
front_part_list = [x for x in front_part.split(' ')]
front_part_list = list(filter(None, front_part_list))
# print(f'FrontPartList =>{FrontPartList}<=')
max_no = int(front_part_list[0].rstrip(')'))
if api.config.log_level == log.INFO:
pb = progressbar.ProgressBar(
max_value=max_no,
redirect_stdout=True)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('F')
cmd_list.append(command.enter)
cmd_list.append('y')
cmd_list.append('0')
cmd = ''.join(cmd_list)
board_list = []
while True:
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
ori_screen = api.connect_core.get_screen_queue()[-1]
# print(OriScreen)
for line in ori_screen.split('\n'):
if '◎' not in line and '●' not in line:
continue
if line.startswith(api.cursor):
line = line[len(api.cursor):]
if '◎' in line:
front_part = line[:line.find('◎')]
else:
front_part = line[:line.find('●')]
front_part_list = [x for x in front_part.split(' ')]
front_part_list = list(filter(None, front_part_list))
number = front_part_list[0]
if ')' in number:
number = number[:number.rfind(')')]
no = int(number)
board_name = front_part_list[1]
if board_name.startswith('ˇ'):
board_name = board_name[1:]
if len(board_name) == 0:
board_name = front_part_list[2]
board_list.append(board_name)
if api.config.log_level == log.INFO:
pb.update(no)
if no >= max_no:
break
cmd = command.ctrl_f
if api.config.log_level == log.INFO:
pb.finish()
return board_list
================================================
FILE: PyPtt/_api_get_bottom_post_list.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import log
from . import screens
def get_bottom_post_list(api, board):
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
check_value.check_type(board, str, 'board')
log.logger.info(i18n.catch_bottom_post)
_api_util.check_board(api, board)
_api_util.goto_board(api, board, end=True)
last_screen = api.connect_core.get_screen_queue()[-1]
bottom_screen = [line for line in last_screen.split('\n') if '★' in line[:8]]
bottom_length = len(bottom_screen)
if bottom_length == 0:
log.logger.info(i18n.catch_bottom_post_success)
return list()
cmd_list = []
cmd_list.append(command.query_post)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(
screens.Target.PTT1_QueryPost if api.config.host == data_type.HOST.PTT1 else screens.Target.PTT2_QueryPost,
log_level=log.DEBUG, break_detect=True, refresh=False),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)),
]
aid_list = []
result = []
for _ in range(bottom_length):
api.connect_core.send(cmd, target_list)
last_screen = api.connect_core.get_screen_queue()[-1]
lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index = \
_api_util.parse_query_post(
api,
last_screen)
aid_list.append(post_aid)
cmd_list = []
cmd_list.append(command.enter)
cmd_list.append(command.up)
cmd_list.append(command.query_post)
cmd = ''.join(cmd_list)
aid_list.reverse()
for post_aid in aid_list:
current_post = api.get_post(board=board, aid=post_aid, query=True)
result.append(current_post)
log.logger.info(i18n.catch_bottom_post, '...', i18n.success)
return list(reversed(result))
================================================
FILE: PyPtt/_api_get_favourite_board.py
================================================
from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from .data_type import FavouriteBoardField
def get_favourite_board(api) -> list:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
cmd_list = [command.go_main_menu, 'F', command.enter, '0']
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('選擇看板', break_detect=True)
]
log.logger.info(i18n.get_favourite_board_list)
board_list = []
favourite_board_list = []
while True:
api.connect_core.send(
cmd,
target_list)
ori_screen = api.connect_core.get_screen_queue()[-1]
# print(OriScreen)
screen_buf = ori_screen
screen_buf = [x for x in screen_buf.split('\n')][3:-1]
# adjust for cursor
screen_buf[0] = ' ' + screen_buf[0][1:]
screen_buf = [x for x in screen_buf]
min_len = 47
for i, line in enumerate(screen_buf):
if len(screen_buf[i]) == 0:
continue
if len(screen_buf[i]) <= min_len:
# print(f'[{ScreenBuf[i]}]')
screen_buf[i] = screen_buf[i] + (' ' * ((min_len + 1) - len(screen_buf[i])))
screen_buf = [x[10:min_len - len(x)].strip() for x in screen_buf]
screen_buf = list(filter(None, screen_buf))
for i, line in enumerate(screen_buf):
if '------------' in line:
continue
temp = line.strip().split(' ')
no_space_temp = list(filter(None, temp))
board = no_space_temp[0]
if board.startswith('ˇ'):
board = board[1:]
board_type = no_space_temp[1]
title_start_index = temp.index(board_type) + 1
board_title = ' '.join(temp[title_start_index:])
# remove ◎
board_title = board_title[1:]
if board in board_list:
log.logger.info(i18n.success)
return favourite_board_list
board_list.append(board)
favourite_board_list.append({
FavouriteBoardField.board: board,
FavouriteBoardField.type: board_type,
FavouriteBoardField.title: board_title})
if len(screen_buf) < 20:
break
cmd = command.ctrl_f
log.logger.info(i18n.get_favourite_board_list, '...', i18n.success)
return favourite_board_list
================================================
FILE: PyPtt/_api_get_newest_index.py
================================================
from __future__ import annotations
import re
from typing import Optional
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type, lib_util
from . import exceptions
from . import i18n
from . import log
from . import screens
def _get_newest_index(api) -> int:
last_screen = api.connect_core.get_screen_queue()[-1]
last_screen_list = last_screen.split('\n')
last_screen_list = last_screen_list[3:]
last_screen_list = '\n'.join([x[:9] for x in last_screen_list])
# print(last_screen_list)
all_index = re.findall(r'\d+', last_screen_list)
if len(all_index) == 0:
return 0
all_index = list(map(int, all_index))
all_index.sort(reverse=True)
# print(all_index)
max_check_range = 6
newest_index = 0
for index_temp in all_index:
need_continue = True
if index_temp > max_check_range:
check_range = max_check_range
else:
check_range = index_temp
for i in range(1, check_range):
if str(index_temp - i) not in last_screen:
need_continue = False
break
if need_continue:
log.logger.debug(i18n.find_newest_index, index_temp)
newest_index = index_temp
break
if newest_index == 0:
raise exceptions.UnknownError('UnknownError')
return newest_index
def get_newest_index(api, index_type: data_type.NewIndex, board: Optional[str] = None,
search_type: data_type.SearchType = None, search_condition: Optional[str] = None,
search_list: Optional[list] = None) -> int:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if search_list is None:
search_list = []
else:
check_value.check_type(search_list, list, 'search_list')
if (search_type, search_condition) != (None, None):
search_list.insert(0, (search_type, search_condition))
for search_type, search_condition in search_list:
check_value.check_type(search_type, data_type.SearchType, 'search_type')
check_value.check_type(search_condition, str, 'search_condition')
check_value.check_type(index_type, data_type.NewIndex, 'index_type')
data_key = f'{index_type}_{board}_{search_list}'
if data_key in api._newest_index_data:
return api._newest_index_data[data_key]
if index_type == data_type.NewIndex.BOARD:
check_value.check_type(board, str, 'board')
_api_util.check_board(api, board)
_api_util.goto_board(api, board)
cmd_list = []
cmd_list.append('1')
cmd_list.append(command.enter)
cmd_list.append('$')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('沒有文章...', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.MainMenu_Exiting,
exceptions_=exceptions.NoSuchBoard(api.config, board)),
]
index = api.connect_core.send(cmd, target_list)
if index < 0:
raise exceptions.NoSuchBoard(api.config, board)
if index == 0:
return 0
normal_newest_index = _get_newest_index(api)
if search_list is not None and len(search_list) > 0:
target_list.insert(2,
connect_core.TargetUnit(
screens.Target.InBoardWithCursor,
log_level=log.DEBUG,
break_detect=True))
cmd_list = _api_util.get_search_condition_cmd(index_type, search_list)
cmd_list.append('1')
cmd_list.append(command.enter)
cmd_list.append('$')
cmd = ''.join(cmd_list)
index = api.connect_core.send(cmd, target_list)
if index < 0:
raise exceptions.NoSuchBoard(api.config, board)
if index == 0:
return 0
newest_index = _get_newest_index(api)
if normal_newest_index == newest_index:
raise exceptions.NoSearchResult()
else:
newest_index = normal_newest_index
elif index_type == data_type.NewIndex.MAIL:
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
if board is not None:
raise ValueError('board should not input at NewIndex.MAIL.')
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
cmd_list.append('1')
cmd_list.append(command.enter)
cmd_list.append('$')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InMailBox, break_detect=True),
connect_core.TargetUnit(screens.Target.CursorToGoodbye, response=cmd),
]
def get_index(api):
current_capacity, _ = _api_util.get_mailbox_capacity(api)
last_screen = api.connect_core.get_screen_queue()[-1]
cursor_line = [x for x in last_screen.split('\n') if x.strip().startswith(api.cursor)][0]
list_index = int(re.compile('(\d+)').search(cursor_line).group(0))
if search_type == 0 and search_list is None:
if list_index > current_capacity:
newest_index = list_index
else:
newest_index = current_capacity
else:
newest_index = list_index
return newest_index
newest_index = 0
index = api.connect_core.send(
cmd,
target_list)
if index == 0:
normal_newest_index = get_index(api)
if search_list is not None and len(search_list) > 0:
target_list.insert(
2,
connect_core.TargetUnit(
screens.Target.InMailBoxWithCursor,
log_level=log.DEBUG,
break_detect=True)
)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
cmd_list.extend(
_api_util.get_search_condition_cmd(index_type, search_list)
)
cmd_list.append('1')
cmd_list.append(command.enter)
cmd_list.append('$')
cmd = ''.join(cmd_list)
index = api.connect_core.send(
cmd,
target_list)
if index in [0, 2]:
newest_index = get_index(api)
if normal_newest_index == newest_index:
raise exceptions.NoSearchResult()
else:
newest_index = normal_newest_index
api._newest_index_data[data_key] = newest_index
return newest_index
================================================
FILE: PyPtt/_api_get_post.py
================================================
from __future__ import annotations
import json
import re
import time
from typing import Dict, Optional
from AutoStrEnum import AutoJsonEncoder
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
from .data_type import PostField, CommentField
def get_post(api, board: str, aid: Optional[str] = None, index: Optional[int] = None,
search_list: Optional[list] = None,
search_type: Optional[data_type.SearchType] = None,
search_condition: Optional[str] = None, query: bool = False) -> Dict:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
check_value.check_type(board, str, 'board')
if aid is not None:
check_value.check_type(aid, str, 'aid')
if index is not None:
check_value.check_type(index, int, 'index')
if search_list is None:
search_list = []
else:
check_value.check_type(search_list, list, 'search_list')
if (search_type, search_condition) != (None, None):
search_list.insert(0, (search_type, search_condition))
for search_type, search_condition in search_list:
check_value.check_type(search_type, data_type.SearchType, 'search_type')
check_value.check_type(search_condition, str, 'search_condition')
if len(board) == 0:
raise ValueError(f'board error parameter: {board}')
if index is not None and isinstance(aid, str):
raise ValueError('wrong parameter index and aid can\'t both input')
if index is None and aid is None:
raise ValueError('wrong parameter index or aid must input')
search_cmd = None
if search_list is not None and len(search_list) > 0:
current_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board, search_list=search_list)
search_cmd = _api_util.get_search_condition_cmd(data_type.NewIndex.BOARD, search_list)
else:
current_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board)
if index is not None:
check_value.check_index('index', index, current_index)
max_retry = 2
post = {}
for i in range(max_retry):
try:
post = _get_post(api, board, aid, index, query, search_cmd)
if not post:
pass
elif not post[PostField.pass_format_check]:
pass
else:
break
except exceptions.UnknownError:
if i == max_retry - 1:
raise
except exceptions.NoSuchBoard:
if i == max_retry - 1:
raise
log.logger.debug('Wait for retry repost')
time.sleep(0.1)
post = json.dumps(post, cls=AutoJsonEncoder)
return json.loads(post)
def _get_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0, query: bool = False,
search_cmd_list: Optional[list[str]] = None) -> Dict:
_api_util.check_board(api, board)
_api_util.goto_board(api, board)
cmd_list = []
if post_aid is not None:
cmd_list.append(lib_util.check_aid(post_aid))
elif post_index != 0:
if search_cmd_list is not None:
cmd_list.extend(search_cmd_list)
cmd_list.append(str(max(1, post_index - 100)))
cmd_list.append(command.enter)
cmd_list.append(str(post_index))
else:
raise ValueError('post_aid and post_index cannot be None at the same time')
cmd_list.append(command.enter)
cmd_list.append(command.query_post)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(
screens.Target.PTT1_QueryPost if api.config.host == data_type.HOST.PTT1 else screens.Target.PTT2_QueryPost,
log_level=log.DEBUG, break_detect=True, refresh=False),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)),
]
index = api.connect_core.send(cmd, target_list)
ori_screen = api.connect_core.get_screen_queue()[-1]
post = {
PostField.board: None,
PostField.aid: None,
PostField.index: None,
PostField.author: None,
PostField.date: None,
PostField.title: None,
PostField.content: None,
PostField.money: None,
PostField.url: None,
PostField.ip: None,
PostField.comments: [],
PostField.post_status: data_type.PostStatus.EXISTS,
PostField.list_date: None,
PostField.has_control_code: False,
PostField.pass_format_check: False,
PostField.location: None,
PostField.push_number: None,
PostField.is_lock: False,
PostField.full_content: None,
PostField.is_unconfirmed: False}
post_author = None
post_title = None
if index < 0 or index == 1:
# 文章被刪除
log.logger.debug(i18n.post_deleted)
log.logger.debug('OriScreen', ori_screen)
cursor_line = [line for line in ori_screen.split(
'\n') if line.startswith(api.cursor)]
if len(cursor_line) != 1:
raise exceptions.UnknownError(ori_screen)
cursor_line = cursor_line[0]
log.logger.debug('CursorLine', cursor_line)
pattern = re.compile('[\d]+\/[\d]+')
pattern_result = pattern.search(cursor_line)
if pattern_result is None:
list_date = None
else:
list_date = pattern_result.group(0)
list_date = list_date[-5:]
pattern = re.compile('\[[\w]+\]')
pattern_result = pattern.search(cursor_line)
if pattern_result is not None:
post_del_status = data_type.PostStatus.DELETED_BY_AUTHOR
else:
pattern = re.compile('<[\w]+>')
pattern_result = pattern.search(cursor_line)
post_del_status = data_type.PostStatus.DELETED_BY_MODERATOR
# > 79843 9/11 - □ (本文已被吃掉)<
# > 76060 8/28 - □ (本文已被刪除) [weida7332]
# print(f'O=>{CursorLine}<')
if pattern_result is not None:
post_author = pattern_result.group(0)[1:-1]
else:
post_author = None
post_del_status = data_type.PostStatus.DELETED_BY_UNKNOWN
log.logger.debug('ListDate', list_date)
log.logger.debug('PostAuthor', post_author)
log.logger.debug('post_del_status', post_del_status)
post.update({
PostField.board: board,
PostField.author: post_author,
PostField.list_date: list_date,
PostField.post_status: post_del_status,
PostField.pass_format_check: True
})
return post
elif index == 0:
lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index = \
_api_util.parse_query_post(
api,
ori_screen)
if lock_post:
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.list_date: list_date,
PostField.pass_format_check: True,
PostField.push_number: push_number,
PostField.is_lock: True})
return post
if query:
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.list_date: list_date,
PostField.pass_format_check: True,
PostField.push_number: push_number})
return post
origin_post, has_control_code = _api_util.get_content(api)
if origin_post is None:
log.logger.info(i18n.post_deleted)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.push_number: push_number,
PostField.is_unconfirmed: api.Unconfirmed
})
return post
post_author_pattern_new = re.compile('作者 (.+) 看板')
post_author_pattern_old = re.compile('作者 (.+)')
board_pattern = re.compile('看板 (.+)')
post_date = None
post_content = None
ip = None
location = None
push_list = []
# 格式確認,亂改的我也沒辦法Q_Q
origin_post_lines = origin_post.split('\n')
author_line = origin_post_lines[0]
if board.lower() == 'allpost':
board_line = author_line[author_line.find(')') + 1:]
pattern_result = board_pattern.search(board_line)
if pattern_result is not None:
board_temp = post_author = pattern_result.group(0)
board_temp = board_temp[2:].strip()
if len(board_temp) > 0:
board = board_temp
log.logger.debug(i18n.board, board)
pattern_result = post_author_pattern_new.search(author_line)
if pattern_result is not None:
post_author = pattern_result.group(0)
post_author = post_author[:post_author.rfind(')') + 1]
else:
pattern_result = post_author_pattern_old.search(author_line)
if pattern_result is None:
log.logger.info(i18n.substandard_post, i18n.author)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed, })
return post
post_author = pattern_result.group(0)
post_author = post_author[:post_author.rfind(')') + 1]
post_author = post_author[4:].strip()
log.logger.debug(i18n.author, post_author)
post_title_pattern = re.compile('標題 (.+)')
title_line = origin_post_lines[1]
pattern_result = post_title_pattern.search(title_line)
if pattern_result is None:
log.logger.info(i18n.substandard_post, i18n.title)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed, })
return post
post_title = pattern_result.group(0)
post_title = post_title[4:].strip()
log.logger.debug(i18n.title, post_title)
post_date_pattern = re.compile('時間 .{24}')
date_line = origin_post_lines[2]
pattern_result = post_date_pattern.search(date_line)
if pattern_result is None:
log.logger.info(i18n.substandard_post, i18n.date)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed, })
return post
post_date = pattern_result.group(0)
post_date = post_date[4:].strip()
log.logger.debug(i18n.date, post_date)
content_fail = True
if screens.Target.content_start not in origin_post:
# print('Type 1')
content_fail = True
else:
post_content = origin_post
post_content = post_content[
post_content.find(screens.Target.content_start) + len(screens.Target.content_start) + 1:]
# print('Type 2')
# print(f'PostContent [{PostContent}]')
for content_end in screens.Target.content_end_list:
# + 3 = 把 --\n 拿掉
# print(f'EC [{EC}]')
if content_end in post_content:
content_fail = False
post_content = post_content[:post_content.rfind(content_end) + 3]
origin_post_lines = origin_post[origin_post.find(content_end):]
# post_content = post_content.strip()
origin_post_lines = origin_post_lines.split('\n')
break
if content_fail:
log.logger.info(i18n.substandard_post, i18n.content)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed, })
return post
log.logger.debug(i18n.content, post_content)
info_lines = [line for line in origin_post_lines if line.startswith('※') or line.startswith('◆')]
pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+')
pattern_p2 = re.compile('[\d]+-[\d]+-[\d]+-[\d]+')
for line in reversed(info_lines):
log.logger.debug('IP Line', line)
# type 1
# ※ 編輯: CodingMan (111.243.146.98 臺灣)
# ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 111.243.146.98 (臺灣)
# type 2
# ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 116.241.32.178
# ※ 編輯: kill77845 (114.136.55.237), 12/08/2018 16:47:59
# type 3
# ※ 發信站: 批踢踢實業坊(ptt.cc)
# ◆ From: 211.20.78.69
# ※ 編輯: JCC 來自: 211.20.78.69 (06/20 10:22)
# ※ 編輯: JCC (118.163.28.150), 12/03/2015 14:25:35
pattern_result = pattern.search(line)
if pattern_result is not None:
ip = pattern_result.group(0)
location_temp = line[line.find(ip) + len(ip):].strip()
location_temp = location_temp.replace('(', '')
location_temp = location_temp[:location_temp.rfind(')')]
location_temp = location_temp.strip()
# print(f'=>[{LocationTemp}]')
if ' ' not in location_temp and len(location_temp) > 0:
location = location_temp
log.logger.debug('Location', location)
break
pattern_result = pattern_p2.search(line)
if pattern_result is not None:
ip = pattern_result.group(0)
ip = ip.replace('-', '.')
# print(f'IP -> [{IP}]')
break
if api.config.host == data_type.HOST.PTT1:
if ip is None:
log.logger.info(i18n.substandard_post, ip)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed, })
return post
log.logger.debug('IP', ip)
push_author_pattern = re.compile('[推|噓|→] [\w| ]+:')
push_date_pattern = re.compile('[\d]+/[\d]+ [\d]+:[\d]+')
push_ip_pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+')
push_list = []
for line in origin_post_lines:
if line.startswith('推'):
comment_type = data_type.CommentType.PUSH
elif line.startswith('噓 '):
comment_type = data_type.CommentType.BOO
elif line.startswith('→ '):
comment_type = data_type.CommentType.ARROW
else:
continue
result = push_author_pattern.search(line)
if result is None:
# 不符合推文格式
continue
push_author = result.group(0)[2:-1].strip()
log.logger.debug(i18n.comment_id, push_author)
result = push_date_pattern.search(line)
if result is None:
continue
push_date = result.group(0)
log.logger.debug(i18n.comment_date, push_date)
comment_ip = None
result = push_ip_pattern.search(line)
if result is not None:
comment_ip = result.group(0)
log.logger.debug(f'{i18n.comment} ip', comment_ip)
push_content = line[line.find(push_author) + len(push_author):]
# PushContent = PushContent.replace(PushDate, '')
if api.config.host == data_type.HOST.PTT1:
push_content = push_content[:push_content.rfind(push_date)]
else:
# → CodingMan:What is Ptt? 推 10/04 13:25
push_content = push_content[:push_content.rfind(push_date) - 2]
if comment_ip is not None:
push_content = push_content.replace(comment_ip, '')
push_content = push_content[push_content.find(':') + 1:].strip()
log.logger.debug(i18n.comment_content, push_content)
current_push = {
CommentField.type: comment_type,
CommentField.author: push_author,
CommentField.content: push_content,
CommentField.ip: comment_ip,
CommentField.time: push_date}
push_list.append(current_push)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: True,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed})
return post
================================================
FILE: PyPtt/_api_get_post_index.py
================================================
from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens
def get_post_index(api, board: str, aid: str) -> int:
_api_util.goto_board(api, board)
cmd_list = []
cmd_list.append('#')
cmd_list.append(aid)
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('找不到這個文章代碼', log_level=log.DEBUG,
exceptions_=exceptions.NoSuchPost(board, aid)),
# 此狀態下無法使用搜尋文章代碼(AID)功能
connect_core.TargetUnit('此狀態下無法使用搜尋文章代碼(AID)功能',
exceptions_=exceptions.CanNotUseSearchPostCode()),
connect_core.TargetUnit('沒有文章...', exceptions_=exceptions.NoSuchPost(board, aid)),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.InBoardWithCursor, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board))
]
index = api.connect_core.send(
cmd,
target_list
)
ori_screen = api.connect_core.get_screen_queue()[-1]
if index < 0:
# print(OriScreen)
raise exceptions.NoSuchBoard(api.config, board)
# if index == 5:
# print(OriScreen)
# raise exceptions.NoSuchBoard(api.config, board)
# print(index)
# print(OriScreen)
screen_list = ori_screen.split('\n')
line = [x for x in screen_list if x.startswith(api.cursor)]
line = line[0]
last_line = screen_list[screen_list.index(line) - 1]
# print(LastLine)
# print(line)
if '編號' in last_line and '人氣:' in last_line:
index = line[1:].strip()
index_fix = False
else:
index = last_line.strip()
index_fix = True
while ' ' in index:
index = index.replace(' ', ' ')
index_list = index.split(' ')
index = index_list[0]
if index == '★':
return 0
index = int(index)
if index_fix:
index += 1
# print(Index)
return index
================================================
FILE: PyPtt/_api_get_time.py
================================================
import re
from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens
pattern = re.compile('[\d]+:[\d][\d]')
def get_time(api) -> str:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('A')
cmd_list.append(command.right)
cmd_list.append(command.left)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
]
index = api.connect_core.send(cmd, target_list)
if index != 0:
return None
ori_screen = api.connect_core.get_screen_queue()[-1]
line_list = ori_screen.split('\n')[-3:]
# 0:00
for line in line_list:
if '星期' in line and '線上' in line and '我是' in line:
result = pattern.search(line)
if result is not None:
return result.group(0)
return None
================================================
FILE: PyPtt/_api_get_user.py
================================================
import json
import re
from typing import Dict
from AutoStrEnum import AutoJsonEncoder
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
from .data_type import UserField
def get_user(api, ptt_id: str) -> Dict:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(ptt_id, str, 'UserID')
if len(ptt_id) < 2:
raise ValueError(f'wrong parameter user_id: {ptt_id}')
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('T')
cmd_list.append(command.enter)
cmd_list.append('Q')
cmd_list.append(command.enter)
cmd_list.append(ptt_id)
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.AnyKey, break_detect=True),
connect_core.TargetUnit(screens.Target.InTalk, break_detect=True),
]
index = api.connect_core.send(
cmd,
target_list)
ori_screen = api.connect_core.get_screen_queue()[-1]
if index == 1:
raise exceptions.NoSuchUser(ptt_id)
# PTT1
# 《ID暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》小康 ($73866)
# 《登入次數》1118 次 (同天內只計一次) 《有效文章》15 篇 (退:0)
# 《目前動態》閱讀文章 《私人信箱》最近無新信件
# 《上次上站》10/06/2019 17:29:49 Sun 《上次故鄉》111.251.231.184
# 《 五子棋 》 0 勝 0 敗 0 和 《象棋戰績》 0 勝 0 敗 0 和
# https://github.com/Truth0906/PTTLibrary
# 強大的 PTT 函式庫
# 提供您 快速 穩定 完整 的 PTT API
# 提供專業的 PTT 機器人諮詢服務
# PTT2
# 《ID暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》家徒四壁 ($0)
# 《登入次數》8 次 (同天內只計一次) 《有效文章》0 篇
# 《目前動態》看板列表 《私人信箱》最近無新信件
# 《上次上站》10/06/2019 17:27:55 Sun 《上次故鄉》111.251.231.184
# 《 五子棋 》 0 勝 0 敗 0 和 《象棋戰績》 0 勝 0 敗 0 和
# 《個人名片》CodingMan 目前沒有名片
lines = ori_screen.split('\n')[1:]
def parse_user_info_from_line(line: str) -> (str, str):
part_0 = line[line.find('》') + 1:]
part_0 = part_0[:part_0.find('《')].strip()
part_1 = line[line.rfind('》') + 1:].strip()
return part_0, part_1
ptt_id, buff_1 = parse_user_info_from_line(lines[0])
money = int(int_list[0]) if len(int_list := re.findall(r'\d+', buff_1)) > 0 else buff_1
buff_0, buff_1 = parse_user_info_from_line(lines[1])
login_count = int(re.findall(r'\d+', buff_0)[0])
account_verified = ('同天內只計一次' in buff_0)
legal_post = int(re.findall(r'\d+', buff_1)[0])
# PTT2 沒有退文
if api.config.host == data_type.HOST.PTT1:
illegal_post = int(re.findall(r'\d+', buff_1)[1])
else:
illegal_post = None
activity, mail = parse_user_info_from_line(lines[2])
last_login_date, last_login_ip = parse_user_info_from_line(lines[3])
five_chess, chess = parse_user_info_from_line(lines[4])
signature_file = '\n'.join(lines[5:-1]).strip('\n')
log.logger.debug('ptt_id', ptt_id)
log.logger.debug('money', money)
log.logger.debug('login_count', login_count)
log.logger.debug('account_verified', account_verified)
log.logger.debug('legal_post', legal_post)
log.logger.debug('illegal_post', illegal_post)
log.logger.debug('activity', activity)
log.logger.debug('mail', mail)
log.logger.debug('last_login_date', last_login_date)
log.logger.debug('last_login_ip', last_login_ip)
log.logger.debug('five_chess', five_chess)
log.logger.debug('chess', chess)
log.logger.debug('signature_file', signature_file)
user = {
UserField.ptt_id: ptt_id,
UserField.money: money,
UserField.login_count: login_count,
UserField.account_verified: account_verified,
UserField.legal_post: legal_post,
UserField.illegal_post: illegal_post,
UserField.activity: activity,
UserField.mail: mail,
UserField.last_login_date: last_login_date,
UserField.last_login_ip: last_login_ip,
UserField.five_chess: five_chess,
UserField.chess: chess,
UserField.signature_file: signature_file,
}
user = json.dumps(user, cls=AutoJsonEncoder)
return json.loads(user)
================================================
FILE: PyPtt/_api_give_money.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import log
def give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag_content: str) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(ptt_id, str, 'ptt_id')
check_value.check_type(money, int, 'money')
if red_bag_title is not None:
check_value.check_type(red_bag_title, str, 'red_bag_title')
else:
red_bag_title = ''
if red_bag_content is not None:
check_value.check_type(red_bag_content, str, 'red_bag_content')
else:
red_bag_content = ''
log.logger.info(
i18n.replace(i18n.give_money_to, ptt_id, money))
# Check data_type.user
api.get_user(ptt_id)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('P')
cmd_list.append(command.enter)
cmd_list.append('P')
cmd_list.append(command.enter)
cmd_list.append('O')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
edit_red_bag_cmd_list = list()
edit_red_bag_target = connect_core.TargetUnit('要修改紅包袋嗎', response='n' + command.enter)
if red_bag_title != '' or red_bag_content != '':
edit_red_bag_cmd_list.append('y')
edit_red_bag_cmd_list.append(command.enter)
if red_bag_title != '':
edit_red_bag_cmd_list.append(command.down)
edit_red_bag_cmd_list.append(command.ctrl_y) # remove the red_bag_title
edit_red_bag_cmd_list.append(command.enter)
edit_red_bag_cmd_list.append(command.up)
edit_red_bag_cmd_list.append(f'標題: {red_bag_title}')
# reset cursor to original position
edit_red_bag_cmd_list.append(command.up * 2)
if red_bag_content != '':
edit_red_bag_cmd_list.append(command.down * 4)
edit_red_bag_cmd_list.append(command.ctrl_y * 8) # remove original red_bag_content
edit_red_bag_cmd_list.append(red_bag_content)
edit_red_bag_cmd_list.append(command.ctrl_x)
edit_red_bag_cmd = ''.join(edit_red_bag_cmd_list)
edit_red_bag_target = connect_core.TargetUnit('要修改紅包袋嗎', response=edit_red_bag_cmd)
target_list = [
connect_core.TargetUnit('你沒有那麼多Ptt幣喔!', break_detect=True, exceptions_=exceptions.NoMoney()),
connect_core.TargetUnit('金額過少,交易取消!', break_detect=True, exceptions_=exceptions.NoMoney()),
connect_core.TargetUnit('交易取消!', break_detect=True,
exceptions_=exceptions.UnknownError(i18n.transaction_cancelled)),
connect_core.TargetUnit('確定進行交易嗎?', response='y' + command.enter),
connect_core.TargetUnit('按任意鍵繼續', break_detect=True),
edit_red_bag_target,
connect_core.TargetUnit('要修改紅包袋嗎', response=command.enter),
connect_core.TargetUnit('完成交易前要重新確認您的身份', response=api._ptt_pw + command.enter),
connect_core.TargetUnit('他是你的小主人,是否匿名?', response='n' + command.enter),
connect_core.TargetUnit('要給他多少Ptt幣呢?', response=command.tab + str(money) + command.enter),
connect_core.TargetUnit('這位幸運兒的id', response=ptt_id + command.enter),
connect_core.TargetUnit('認證尚未過期', response='y' + command.enter),
connect_core.TargetUnit('交易正在進行中', response=command.space)
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout
)
log.logger.info(
i18n.replace(i18n.give_money_to, ptt_id, money),
'...', i18n.success)
================================================
FILE: PyPtt/_api_has_new_mail.py
================================================
import re
from . import _api_util
from . import command
from . import connect_core
from . import log
from . import screens
def has_new_mail(api) -> int:
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
# cmd_list.append('1')
# cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
current_capacity = None
plus_count = 0
index_pattern = re.compile('(\d+)')
checked_index_list = []
break_detect = False
target_list = [
connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True)
]
api.connect_core.send(
cmd,
target_list,
)
current_capacity, _ = _api_util.get_mailbox_capacity(api)
if current_capacity > 20:
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
cmd_list.append('1')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
while True:
if current_capacity > 20:
api.connect_core.send(
cmd,
target_list,
)
last_screen = api.connect_core.get_screen_queue()[-1]
last_screen_list = last_screen.split('\n')
last_screen_list = last_screen_list[3:-1]
last_screen_list = [x[:10] for x in last_screen_list]
current_plus_count = 0
for line in last_screen_list:
if str(current_capacity) in line:
break_detect = True
index_result = index_pattern.search(line)
if index_result is None:
continue
current_index = index_result.group(0)
if current_index in checked_index_list:
continue
checked_index_list.append(current_index)
if '+' not in line:
continue
current_plus_count += 1
plus_count += current_plus_count
if break_detect:
break
cmd = command.ctrl_f
return plus_count
================================================
FILE: PyPtt/_api_loginout.py
================================================
import re
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import log
from . import screens
def logout(api) -> None:
_api_util.one_thread(api)
log.logger.info(i18n.logout)
if not api._is_login:
log.logger.info(i18n.logout, '...', i18n.success)
return
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('g')
cmd_list.append(command.enter)
cmd_list.append('y')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('任意鍵', break_detect=True),
]
try:
api.connect_core.send(cmd, target_list)
api.connect_core.close()
except exceptions.ConnectionClosed:
pass
except RuntimeError:
pass
api._is_login = False
log.logger.info(i18n.logout, '...', i18n.success)
def login(api, ptt_id: str, ptt_pw: str, kick_other_session: bool):
_api_util.one_thread(api)
check_value.check_type(ptt_id, str, 'ptt_id')
check_value.check_type(ptt_pw, str, 'password')
check_value.check_type(kick_other_session, bool, 'kick_other_session')
if api._is_login:
api.logout()
api.config.kick_other_session = kick_other_session
def kick_other_login_display_msg():
if api.config.kick_other_session:
return i18n.kick_other_login
return i18n.not_kick_other_login
def kick_other_login_response(screen):
if api.config.kick_other_session:
return 'y' + command.enter
return 'n' + command.enter
api.is_mailbox_full = False
# def is_mailbox_full():
# log.log(
# api.config,
# LogLevel.INFO,
# i18n.MailBoxFull)
# api.is_mailbox_full = True
def register_processing(screen):
pattern = re.compile('[\d]+')
api.process_picks = int(pattern.search(screen).group(0))
if len(ptt_pw) > 8:
ptt_pw = ptt_pw[:8]
ptt_id = ptt_id.strip()
ptt_pw = ptt_pw.strip()
api.ptt_id = ptt_id
api._ptt_pw = ptt_pw
api.connect_core.connect()
log.logger.info(i18n.login_id, ptt_id)
target_list = [
connect_core.TargetUnit(screens.Target.InMailBox,
response=command.go_main_menu + 'A' + command.right + command.left, break_detect=True),
connect_core.TargetUnit(screens.Target.InMailMenu, response=command.go_main_menu),
connect_core.TargetUnit(screens.Target.MainMenu, break_detect=True),
connect_core.TargetUnit('【看板列表】', response=command.go_main_menu),
connect_core.TargetUnit('密碼不對', break_detect=True, exceptions_=exceptions.WrongIDorPassword()),
connect_core.TargetUnit('請重新輸入', break_detect=True, exceptions_=exceptions.WrongIDorPassword()),
connect_core.TargetUnit('登入太頻繁', response=' '),
connect_core.TargetUnit('系統過載', break_detect=True),
connect_core.TargetUnit('您要刪除以上錯誤嘗試的記錄嗎', response='y' + command.enter),
connect_core.TargetUnit('請選擇暫存檔 (0-9)[0]', response=command.enter),
connect_core.TargetUnit('有一篇文章尚未完成', response='Q' + command.enter),
connect_core.TargetUnit(
'請重新設定您的聯絡信箱', break_detect=True, exceptions_=exceptions.ResetYourContactEmail()),
# connect_core.TargetUnit(
# i18n.in_login_process_please_wait,
# '登入中,請稍候'),
connect_core.TargetUnit('密碼正確'),
# 密碼正確
connect_core.TargetUnit('您想刪除其他重複登入的連線嗎', response=kick_other_login_response),
connect_core.TargetUnit('◆ 您的註冊申請單尚在處理中', response=command.enter, handler=register_processing),
connect_core.TargetUnit('任意鍵', response=' '),
connect_core.TargetUnit('正在更新與同步線上使用者及好友名單'),
connect_core.TargetUnit('【分類看板】', response=command.go_main_menu),
connect_core.TargetUnit([
'大富翁',
'排行榜',
'名次',
'代號',
'暱稱',
'數目'
], response=command.go_main_menu),
connect_core.TargetUnit([
'熱門話題'
], response=command.go_main_menu),
connect_core.TargetUnit('您確定要填寫註冊單嗎', response=command.enter * 3),
connect_core.TargetUnit('以上資料是否正確', response='y' + command.enter),
connect_core.TargetUnit('另外若輸入後發生認證碼錯誤請先確認輸入是否為最後一封', response='x' + command.enter),
connect_core.TargetUnit('此帳號已設定為只能使用安全連線', exceptions_=exceptions.OnlySecureConnection())
]
# IAC = '\xff'
# WILL = '\xfb'
# NAWS = '\x1f'
cmd_list = []
# cmd_list.append(IAC + WILL + NAWS)
cmd_list.append(ptt_id + ',')
cmd_list.append(command.enter)
cmd_list.append(ptt_pw)
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
index = api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout,
refresh=False,
secret=True)
if index == 0:
current_capacity, max_capacity = _api_util.get_mailbox_capacity(api)
log.logger.info(i18n.has_new_mail_goto_main_menu)
if current_capacity > max_capacity:
api.is_mailbox_full = True
log.logger.info(i18n.mail_box_full)
if api.is_mailbox_full:
log.logger.info(i18n.use_mailbox_api_will_logout_after_execution)
target_list = [
connect_core.TargetUnit(screens.Target.MainMenu, break_detect=True)
]
cmd = command.go_main_menu + 'A' + command.right + command.left
index = api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout,
secret=True)
ori_screen = api.connect_core.get_screen_queue()[-1]
is_login = True
for t in screens.Target.MainMenu:
if t not in ori_screen:
is_login = False
break
if not is_login:
raise exceptions.LoginError()
if '> (' in ori_screen:
api.cursor = data_type.Cursor.NEW
log.logger.debug(i18n.new_cursor)
elif '●(' in ori_screen:
api.cursor = data_type.Cursor.OLD
log.logger.debug(i18n.old_cursor)
else:
raise exceptions.UnknownError()
screens.Target.InBoardWithCursor = screens.Target.InBoardWithCursor[:screens.Target.InBoardWithCursorLen]
screens.Target.InBoardWithCursor.append(api.cursor)
screens.Target.InMailBoxWithCursor = screens.Target.InMailBoxWithCursor[:screens.Target.InMailBoxWithCursorLen]
screens.Target.InMailBoxWithCursor.append(api.cursor)
screens.Target.CursorToGoodbye = screens.Target.CursorToGoodbye[:len(screens.Target.MainMenu)]
if api.cursor == '>':
screens.Target.CursorToGoodbye.append('> (G)oodbye')
else:
screens.Target.CursorToGoodbye.append('●(G)oodbye')
unregistered_user = True
if '(T)alk' in ori_screen:
unregistered_user = False
if '(P)lay' in ori_screen:
unregistered_user = False
if '(N)amelist' in ori_screen:
unregistered_user = False
if unregistered_user:
log.logger.info(i18n.unregistered_user_cant_use_all_api)
api.is_registered_user = not unregistered_user
if api.process_picks != 0:
log.logger.info(i18n.picks_in_register, api.process_picks)
api._is_login = True
log.logger.info(i18n.login_success)
================================================
FILE: PyPtt/_api_mail.py
================================================
from __future__ import annotations
import re
from typing import Dict, Optional
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
from .data_type import MailField
# 寄信
def mail(api,
ptt_id: str,
title: str,
content: str,
sign_file,
backup: bool = True) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(ptt_id, str, 'ptt_id')
check_value.check_type(title, str, 'title')
check_value.check_type(content, str, 'content')
api.get_user(ptt_id)
check_sign_file = False
for i in range(0, 10):
if str(i) == sign_file or i == sign_file:
check_sign_file = True
break
if not check_sign_file:
if sign_file.lower() != 'x':
raise ValueError(f'wrong parameter sign_file: {sign_file}')
cmd_list = []
# 回到主選單
cmd_list.append(command.go_main_menu)
# 私人信件區
cmd_list.append('M')
cmd_list.append(command.enter)
# 站內寄信
cmd_list.append('S')
cmd_list.append(command.enter)
# 輸入 id
cmd_list.append(ptt_id)
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
# 定義如何根據情況回覆訊息
target_list = [
connect_core.TargetUnit('主題:', break_detect=True),
connect_core.TargetUnit('【電子郵件】', exceptions_=exceptions.NoSuchUser(ptt_id))
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout
)
cmd_list = []
# 輸入標題
cmd_list.append(title)
cmd_list.append(command.enter)
# 輸入內容
cmd_list.append(content)
# 儲存檔案
cmd_list.append(command.ctrl_x)
cmd = ''.join(cmd_list)
# 定義如何根據情況回覆訊息
target_list = [
connect_core.TargetUnit('請按任意鍵繼續', response=command.enter, break_detect_after_send=True),
connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
connect_core.TargetUnit('是否自存底稿', response=('y' if backup else 'n') + command.enter),
connect_core.TargetUnit('選擇簽名檔', response=str(sign_file) + command.enter),
connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),
]
# 送出訊息
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_post_timeout)
log.logger.info(i18n.send_mail, i18n.success)
# --
# ※ 發信站: 批踢踢實業坊(ptt.cc)
# ◆ From: 220.142.14.95
content_start = '───────────────────────────────────────'
content_end = '--\n※ 發信站: 批踢踢實業坊(ptt.cc)'
content_ip_old = '◆ From: '
mail_author_pattern = re.compile('作者 (.+)')
mail_title_pattern = re.compile('標題 (.+)')
mail_date_pattern = re.compile('時間 (.+)')
ip_pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+')
def get_mail(api, index: int, search_type: Optional[data_type.SearchType] = None,
search_condition: Optional[str] = None,
search_list: Optional[list] = None) -> Dict:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
log.logger.info(i18n.get_mail)
if not isinstance(index, int):
raise ValueError('index must be int')
current_index = api.get_newest_index(data_type.NewIndex.MAIL)
if index <= 0 or current_index < index:
raise exceptions.NoSuchMail()
# check_value.check_index('index', index, current_index)
cmd_list = []
# 回到主選單
cmd_list.append(command.go_main_menu)
# 進入信箱
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
# 處理條件整理出指令
_cmd_list = _api_util.get_search_condition_cmd(data_type.NewIndex.MAIL, search_list)
cmd_list.extend(_cmd_list)
# 前進至目標信件位置
cmd_list.append(str(index))
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
# 有時候會沒有最底下一列,只好偵測游標是否出現
if api.cursor == data_type.Cursor.NEW:
space_length = 6 - len(api.cursor) - len(str(index))
else:
space_length = 5 - len(api.cursor) - len(str(index))
fast_target = f"{api.cursor}{' ' * space_length}{index}"
# 定義如何根據情況回覆訊息
target_list = [
connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(fast_target, log_level=log.DEBUG, break_detect=True)
]
# 送出訊息
api.connect_core.send(
cmd,
target_list)
# 取得信件全文
origin_mail, _ = _api_util.get_content(api, post_mode=False)
# 使用表示式分析信件作者
pattern_result = mail_author_pattern.search(origin_mail)
if pattern_result is None:
mail_author = None
else:
mail_author = pattern_result.group(0)[2:].strip()
# 使用表示式分析信件標題
pattern_result = mail_title_pattern.search(origin_mail)
if pattern_result is None:
mail_title = None
else:
mail_title = pattern_result.group(0)[2:].strip()
# 使用表示式分析信件日期
pattern_result = mail_date_pattern.search(origin_mail)
if pattern_result is None:
mail_date = None
else:
mail_date = pattern_result.group(0)[2:].strip()
# 從全文拿掉信件開頭作為信件內文
mail_content = origin_mail[origin_mail.find(content_start) + len(content_start) + 1:]
mail_content = mail_content[
mail_content.find(screens.Target.content_start) + len(screens.Target.content_start) + 1:]
for EC in screens.Target.content_end_list:
# + 3 = 把 --\n 拿掉
if EC in mail_content:
mail_content = mail_content[:mail_content.rfind(EC) + 3].rstrip()
break
# 紅包偵測
red_envelope = False
if content_end not in origin_mail and 'Ptt幣的大紅包喔' in origin_mail:
mail_content = mail_content.strip()
red_envelope = True
else:
mail_content = mail_content[:mail_content.rfind(content_end) + 3]
if red_envelope:
mail_ip = None
mail_location = None
else:
# ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 111.242.182.114
# ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 59.104.127.126 (臺灣)
# 非紅包開始解析 ip 與 地區
ip_line_list = origin_mail.split('\n')
ip_line = [x for x in ip_line_list if x.startswith(content_end[3:])]
if len(ip_line) == 0:
# 沒 ip 就沒地區
mail_ip = None
mail_location = None
else:
ip_line = ip_line[0]
result = ip_pattern.search(ip_line)
if result is None:
ip_line = [x for x in ip_line_list if x.startswith(content_ip_old)]
if len(ip_line) == 0:
mail_ip = None
else:
ip_line = ip_line[0]
result = ip_pattern.search(ip_line)
mail_ip = result.group(0)
else:
mail_ip = result.group(0)
location = ip_line[ip_line.find(mail_ip) + len(mail_ip):].strip()
if len(location) == 0:
mail_location = None
else:
# print(location)
mail_location = location[1:-1]
log.logger.info(i18n.get_mail, '...', i18n.success)
return {
MailField.origin_mail: origin_mail,
MailField.author: mail_author,
MailField.title: mail_title,
MailField.date: mail_date,
MailField.content: mail_content,
MailField.ip: mail_ip,
MailField.location: mail_location,
MailField.is_red_envelope: red_envelope}
def del_mail(api, index) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
current_index = api.get_newest_index(data_type.NewIndex.MAIL)
check_value.check_index(index, current_index)
cmd_list = []
# 進入主選單
cmd_list.append(command.go_main_menu)
# 進入信箱
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
if index > 20:
# speed up
cmd_list.append(str(1))
cmd_list.append(command.enter)
# 前進到目標信件位置
cmd_list.append(str(index))
cmd_list.append(command.enter)
# 刪除
cmd_list.append('dy')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
# 定義如何根據情況回覆訊息
target_list = [
connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True)
]
# 送出
api.connect_core.send(
cmd,
target_list)
if api.is_mailbox_full:
api.logout()
raise exceptions.MailboxFull()
================================================
FILE: PyPtt/_api_mark_post.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
def mark_post(api, mark_type: int, board: str, post_aid: str, post_index: int, search_type: int,
search_condition: str) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
if not isinstance(mark_type, data_type.MarkType):
raise TypeError(f'mark_type must be data_type.MarkType')
check_value.check_type(board, str, 'board')
if post_aid is not None:
check_value.check_type(post_aid, str, 'PostAID')
check_value.check_type(post_index, int, 'PostIndex')
if not isinstance(search_type, data_type.SearchType):
raise TypeError(f'search_type must be data_type.SearchType')
if search_condition is not None:
check_value.check_type(search_condition, str, 'SearchCondition')
if len(board) == 0:
raise ValueError(f'board error parameter: {board}')
if mark_type != data_type.MarkType.DELETE_D:
if post_index != 0 and isinstance(post_aid, str):
raise ValueError('wrong parameter index and aid can\'t both input')
if post_index == 0 and post_aid is None:
raise ValueError('wrong parameter index or aid must input')
if search_condition is not None and search_type == 0:
raise ValueError('wrong parameter index or aid must input')
if search_type == data_type.SearchType.COMMENT:
try:
S = int(search_condition)
except ValueError:
raise ValueError(f'wrong parameter search_condition: {search_condition}')
check_value.check_range(S, -100, 100, 'search_condition')
if post_aid is not None and search_condition is not None:
raise ValueError('wrong parameter aid and search_condition can\'t both input')
if post_index != 0:
newest_index = api.get_newest_index(
data_type.NewIndex.BOARD,
board=board,
search_type=search_type,
search_condition=search_condition)
check_value.check_index(
'index',
post_index,
max_value=newest_index)
if mark_type == data_type.MarkType.UNCONFIRMED:
# 批踢踢兔沒有待證文章功能 QQ
if api.config.host == data_type.HOST.PTT2:
raise exceptions.HostNotSupport(lib_util.get_current_func_name())
_api_util.check_board(
board,
check_moderator=True)
_api_util.goto_board(api, board)
cmd_list = []
if post_aid is not None:
cmd_list.append(lib_util.check_aid(post_aid))
cmd_list.append(command.enter)
elif post_index != 0:
if search_condition is not None:
if search_type == data_type.SearchType.KEYWORD:
cmd_list.append('/')
elif search_type == data_type.SearchType.AUTHOR:
cmd_list.append('a')
elif search_type == data_type.SearchType.COMMENT:
cmd_list.append('Z')
elif search_type == data_type.SearchType.MARK:
cmd_list.append('G')
elif search_type == data_type.SearchType.MONEY:
cmd_list.append('A')
cmd_list.append(search_condition)
cmd_list.append(command.enter)
cmd_list.append(str(post_index))
cmd_list.append(command.enter)
else:
raise ValueError('post_aid and post_index cannot be None at the same time')
if mark_type == data_type.MarkType.S:
cmd_list.append('L')
elif mark_type == data_type.MarkType.D:
cmd_list.append('t')
elif mark_type == data_type.MarkType.DELETE_D:
cmd_list.append(command.ctrl_d)
elif mark_type == data_type.MarkType.M:
cmd_list.append('m')
elif mark_type == data_type.MarkType.UNCONFIRMED:
cmd_list.append(command.ctrl_e + 'S')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('刪除所有標記', log_level=log.INFO, response='y' + command.enter),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.INFO, break_detect=True),
]
api.connect_core.send(cmd, target_list)
================================================
FILE: PyPtt/_api_post.py
================================================
from __future__ import annotations
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import log
def fast_post_step0(api, board: str, title: str, content: str, post_type: int) -> None:
_api_util.goto_board(api, board)
cmd_list = []
cmd_list.append(command.ctrl_p)
cmd_list.append(str(post_type))
cmd_list.append(command.enter)
cmd_list.append(str(title))
cmd_list.append(command.enter)
cmd_list.append(str(content))
cmd_list.append(command.ctrl_x)
cmd_list.append('s')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('發表文章於【', break_detect=True),
connect_core.TargetUnit('使用者不可發言', break_detect=True),
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
connect_core.TargetUnit('確定要儲存檔案嗎', break_detect=True)
]
index = api.connect_core.fast_send(cmd, target_list)
if index < 0:
raise exceptions.UnknownError('UnknownError')
if index == 1 or index == 2:
raise exceptions.NoPermission(i18n.no_permission)
def fast_post_step1(api: object, sign_file) -> None:
cmd = '\r'
target_list = [
connect_core.TargetUnit('發表文章於【', break_detect=True),
connect_core.TargetUnit('使用者不可發言', break_detect=True),
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
connect_core.TargetUnit('確定要儲存檔案嗎', break_detect=True),
connect_core.TargetUnit('x=隨機', response=str(sign_file) + '\r'),
]
index = api.connect_core.fast_send(cmd, target_list)
if index < 0:
raise exceptions.UnknownError('UnknownError')
def fast_post(
api: object,
board: str,
title: str,
content: str,
post_type: int,
sign_file) -> None:
_api_util.goto_board(api, board)
cmd_list = []
cmd_list.append(command.ctrl_p)
cmd_list.append(str(post_type))
cmd_list.append(command.enter)
cmd_list.append(str(title))
cmd_list.append(command.enter)
cmd_list.append(str(content))
cmd_list.append(command.ctrl_x)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('發表文章於【', break_detect=True),
connect_core.TargetUnit('使用者不可發言', break_detect=True),
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),
]
index = api.connect_core.fast_send(cmd, target_list)
if index < 0:
raise exceptions.UnknownError('UnknownError')
if index == 1 or index == 2:
raise exceptions.NoPermission(i18n.no_permission)
sign_file_list = [str(x) for x in range(0, 10)]
sign_file_list.append('x')
def post(api, board: str, title: str, content: str, title_index: int, sign_file: [str | int]) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(board, str, 'board')
check_value.check_type(title_index, int, 'title_index')
check_value.check_type(title, str, 'title')
check_value.check_type(content, str, 'content')
if str(sign_file).lower() not in sign_file_list:
raise ValueError(f'wrong parameter sign_file: {sign_file}')
_api_util.check_board(api, board)
_api_util.goto_board(api, board)
log.logger.info(i18n.post)
cmd_list = []
cmd_list.append(command.ctrl_p)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('發表文章於【', break_detect=True),
connect_core.TargetUnit('使用者不可發言', break_detect=True),
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
]
index = api.connect_core.send(cmd, target_list)
if index < 0:
raise exceptions.UnknownError('UnknownError')
if index == 1 or index == 2:
log.logger.info(i18n.post, '...', i18n.fail)
raise exceptions.NoPermission(i18n.no_permission)
log.logger.debug(i18n.has_post_permission)
content = lib_util.uniform_new_line(content)
cmd_list = []
cmd_list.append(str(title_index))
cmd_list.append(command.enter)
cmd_list.append(str(title))
cmd_list.append(command.enter)
cmd_list.append(command.ctrl_y * 40)
cmd_list.append(str(content))
cmd_list.append(command.ctrl_x)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_post_timeout)
log.logger.info(i18n.post, '...', i18n.success)
================================================
FILE: PyPtt/_api_reply_post.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: str, sign_file, post_aid: str,
post_index: int) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not isinstance(reply_to, data_type.ReplyTo):
raise TypeError(f'ReplyTo must be data_type.ReplyTo')
check_value.check_type(board, str, 'board')
check_value.check_type(content, str, 'content')
if post_aid is not None:
check_value.check_type(post_aid, str, 'PostAID')
if post_index != 0:
newest_index = api.get_newest_index(
data_type.NewIndex.BOARD,
board=board)
check_value.check_index(
'index',
post_index,
max_value=newest_index)
sign_file_list = ['x']
sign_file_list.extend([str(x) for x in range(0, 10)])
if str(sign_file).lower() not in sign_file_list:
raise ValueError(f'wrong parameter sign_file: {sign_file}')
if post_aid is not None and post_index != 0:
raise ValueError('wrong parameter aid and index can\'t both input')
_api_util.check_board(api, board)
_api_util.goto_board(api, board)
cmd_list = []
if post_aid is not None:
cmd_list.append(lib_util.check_aid(post_aid))
elif post_index != 0:
cmd_list.append(str(post_index))
else:
raise ValueError('post_aid and post_index cannot be None at the same time')
cmd_list.append(command.enter * 2)
cmd_list.append('r')
if reply_to == data_type.ReplyTo.BOARD:
log.logger.info(i18n.reply_board)
reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='F' + command.enter)
elif reply_to == data_type.ReplyTo.MAIL:
log.logger.info(i18n.reply_mail)
reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='M' + command.enter)
elif reply_to == data_type.ReplyTo.BOARD_MAIL:
log.logger.info(i18n.reply_board_mail)
reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='B' + command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
connect_core.TargetUnit('◆ 很抱歉, 此文章已結案並標記, 不得回應', log_level=log.INFO,
exceptions_=exceptions.CantResponse()),
connect_core.TargetUnit('(E)繼續編輯 (W)強制寫入', log_level=log.INFO, response='W' + command.enter),
connect_core.TargetUnit('請選擇簽名檔', response=str(sign_file) + command.enter),
connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
connect_core.TargetUnit('編輯文章', log_level=log.INFO,
response=str(content) + command.enter + command.ctrl_x),
connect_core.TargetUnit('請問要引用原文嗎', log_level=log.DEBUG, response='Y' + command.enter),
connect_core.TargetUnit('採用原標題[Y/n]?', log_level=log.DEBUG, response='Y' + command.enter),
reply_target_unit,
connect_core.TargetUnit('已順利寄出,是否自存底稿', log_level=log.DEBUG, response='Y' + command.enter),
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
log.logger.info(i18n.success)
================================================
FILE: PyPtt/_api_search_user.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import log
def search_user(api, ptt_id: str, min_page: int, max_page: int) -> list:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(ptt_id, str, 'ptt_id')
if min_page is not None:
check_value.check_index('min_page', min_page)
if max_page is not None:
check_value.check_index('max_page', max_page)
if min_page is not None and max_page is not None:
check_value.check_index_range('min_page', min_page, 'max_page', max_page)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('T')
cmd_list.append(command.enter)
cmd_list.append('Q')
cmd_list.append(command.enter)
cmd_list.append(ptt_id)
cmd = ''.join(cmd_list)
if min_page is not None:
template = min_page
else:
template = 1
appendstr = ' ' * template
cmdtemp = cmd + appendstr
target_list = [
connect_core.TargetUnit('任意鍵', break_detect=True)]
resultlist = []
log.logger.info(i18n.search_user)
while True:
api.connect_core.send(
cmdtemp,
target_list)
ori_screen = api.connect_core.get_screen_queue()[-1]
# print(OriScreen)
# print(len(OriScreen.split('\n')))
if len(ori_screen.split('\n')) == 2:
result_id = ori_screen.split('\n')[1]
result_id = result_id[result_id.find(' ') + 1:].strip()
# print(result_id)
resultlist.append(result_id)
break
else:
ori_screen = ori_screen.split('\n')[3:-1]
ori_screen = '\n'.join(ori_screen)
templist = ori_screen.replace('\n', ' ')
while ' ' in templist:
templist = templist.replace(' ', ' ')
templist = templist.split(' ')
resultlist.extend(templist)
# print(templist)
# print(len(templist))
if len(templist) != 100 and len(templist) != 120:
break
template += 1
if max_page is not None:
if template > max_page:
break
cmdtemp = ' '
api.connect_core.send(
command.enter,
[
# 《ID暱稱》
connect_core.TargetUnit('《ID暱稱》', response=command.enter),
connect_core.TargetUnit('查詢網友', break_detect=True)
]
)
log.logger.info(i18n.success)
return list(filter(None, resultlist))
================================================
FILE: PyPtt/_api_set_board_title.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
def set_board_title(api, board: str, new_title: str) -> None:
# 第一支板主專用 api
_api_util.one_thread(api)
_api_util.goto_board(api, board)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(board, str, 'board')
check_value.check_type(new_title, str, 'new_title')
_api_util.check_board(
api,
board,
check_moderator=True)
cmd_list = []
cmd_list.append('I')
cmd_list.append(command.ctrl_p)
cmd_list.append('b')
cmd_list.append(command.enter)
cmd_list.append(command.backspace * 31)
cmd_list.append(new_title)
cmd_list.append(command.enter * 2)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('◆ 已儲存新設定', break_detect=True),
connect_core.TargetUnit('◆ 未改變任何設定', break_detect=True),
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
================================================
FILE: PyPtt/_api_util.py
================================================
from __future__ import annotations
import functools
import re
import threading
from typing import Dict, Optional
from . import _api_get_board_info
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import log
from . import screens
def get_content(api, post_mode: bool = True):
api.Unconfirmed = False
def is_unconfirmed_handler(screen):
api.Unconfirmed = True
if post_mode:
cmd = command.enter * 2
else:
cmd = command.enter
target_list = [
# 待證實文章
connect_core.TargetUnit('本篇文章內容經站方授權之板務管理人員判斷有尚待證實之處', response=' ',
handler=is_unconfirmed_handler),
connect_core.TargetUnit(screens.Target.PostEnd, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.InPost, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.PostNoContent, log_level=log.DEBUG, break_detect=True),
# 動畫文章
connect_core.TargetUnit(screens.Target.Animation, response=command.go_main_menu_type_q,
break_detect_after_send=True),
]
line_from_pattern = re.compile('[\d]+~[\d]+')
has_control_code = False
control_code_mode = False
push_start = False
content_start_exist = False
content_start_jump = False
content_start_jump_set = False
first_page = True
origin_post = []
stop_dict = dict()
while True:
index = api.connect_core.send(cmd, target_list)
if index == 3 or index == 4:
return None, False
last_screen = api.connect_core.get_screen_queue()[-1]
lines = last_screen.split('\n')
last_line = lines[-1]
lines.pop()
last_screen = '\n'.join(lines)
if screens.Target.content_start in last_screen and not content_start_exist:
content_start_exist = True
if content_start_exist:
if not content_start_jump_set:
if screens.Target.content_start not in last_screen:
content_start_jump = True
content_start_jump_set = True
else:
content_start_jump = False
pattern_result = line_from_pattern.search(last_line)
if pattern_result is None:
control_code_mode = True
has_control_code = True
else:
last_read_line_list = pattern_result.group(0).split('~')
last_read_line_a_temp = int(last_read_line_list[0])
last_read_line_b_temp = int(last_read_line_list[1])
if control_code_mode:
last_read_line_a = last_read_line_a_temp - 1
last_read_line_b = last_read_line_b_temp - 1
control_code_mode = False
if first_page:
first_page = False
origin_post.append(last_screen)
else:
# 這裡是根據觀察畫面行數的變化歸納出的神奇公式...
# 輸出的結果是要判斷出畫面的最後 x 行是新的文章內容
#
# 這裡是 PyPtt 最黑暗最墮落的地方,所有你所知的程式碼守則,在這裡都不適用
# 每除完一次錯誤,我會陷入嚴重的創傷後壓力症候群,而我的腦袋會自動選擇遺忘這裡所有的一切
# 以確保下一個週一,我可以正常上班
# but it works!
# print(LastScreen)
# print(f'last_read_line_a_temp [{last_read_line_a_temp}]')
# print(f'last_read_line_b_temp [{last_read_line_b_temp}]')
# print(f'last_read_line_a {last_read_line_a}')
# print(f'last_read_line_b {last_read_line_b}')
# print(f'GetLineB {last_read_line_a_temp - last_read_line_a}')
# print(f'GetLineA {last_read_line_b_temp - last_read_line_b}')
# print(f'show line {last_read_line_b_temp - last_read_line_a_temp + 1}')
if not control_code_mode:
if last_read_line_a_temp in stop_dict:
new_content_part = '\n'.join(
lines[-stop_dict[last_read_line_a_temp]:])
stop_dict = dict()
else:
get_line_b = last_read_line_b_temp - last_read_line_b
if get_line_b > 0:
# print('Type 1')
new_content_part = '\n'.join(lines[-get_line_b:])
if index == 1 and len(new_content_part) == get_line_b - 1:
new_content_part = '\n'.join(lines[-(get_line_b * 2):])
elif origin_post:
last_line_temp = origin_post[-1].strip()
try_line = lines[-(get_line_b + 1)].strip()
if not last_line_temp.endswith(try_line):
new_content_part = try_line + '\n' + new_content_part
stop_dict = dict()
else:
# 駐足現象,LastReadLineB跟上一次相比並沒有改變
if (last_read_line_b_temp + 1) not in stop_dict:
stop_dict[last_read_line_b_temp + 1] = 1
stop_dict[last_read_line_b_temp + 1] += 1
get_line_a = last_read_line_a_temp - last_read_line_a
if get_line_a > 0:
# print(f'Type 2 get_line_a [{get_line_a}]')
new_content_part = '\n'.join(lines[-get_line_a:])
else:
new_content_part = '\n'.join(lines)
else:
new_content_part = lines[-1]
origin_post.append(new_content_part)
log.logger.debug('NewContentPart', new_content_part)
if index == 1:
if content_start_jump and len(new_content_part) == 0:
get_line_b += 1
new_content_part = '\n'.join(lines[-get_line_b:])
origin_post.pop()
origin_post.append(new_content_part)
break
if not control_code_mode:
last_read_line_a = last_read_line_a_temp
last_read_line_b = last_read_line_b_temp
for EC in screens.Target.content_end_list:
if EC in last_screen:
push_start = True
break
if push_start:
cmd = command.right
else:
cmd = command.down
# print(api.Unconfirmed)
origin_post = '\n'.join(origin_post)
# OriginPost = [line.strip() for line in OriginPost.split('\n')]
# OriginPost = '\n'.join(OriginPost)
log.logger.debug('OriginPost', origin_post)
return origin_post, has_control_code
mail_capacity: Optional[tuple[int, int]] = None
def get_mailbox_capacity(api) -> tuple[int, int]:
global mail_capacity
if mail_capacity is not None:
return mail_capacity
last_screen = api.connect_core.get_screen_queue()[-1]
capacity_line = last_screen.split('\n')[2]
log.logger.debug('capacity_line', capacity_line)
pattern_result = re.compile('(\d+)/(\d+)').search(capacity_line)
if pattern_result is not None:
current_capacity = int(pattern_result.group(0).split('/')[0])
max_capacity = int(pattern_result.group(0).split('/')[1])
log.logger.debug('current_capacity', current_capacity)
log.logger.debug('max_capacity', max_capacity)
mail_capacity = (current_capacity, max_capacity)
return current_capacity, max_capacity
return 0, 0
# > 1 112/09 ericsk □ [心得] 終於開板了
# ┌── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─┐
# │ 文章代碼(AID): #13cPSYOX (Python) [ptt.cc] [心得] 終於開板了 │
# │ 文章網址: https://www.ptt.cc/bbs/Python/M.1134139170.A.621.html │
# │ 這一篇文章值 2 Ptt幣 │
# └── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─┘
def parse_query_post(api, ori_screen):
lock_post = False
try:
cursor_line = [line for line in ori_screen.split(
'\n') if line.strip().startswith(api.cursor)][0]
except Exception as e:
print(api.cursor)
print(ori_screen)
raise e
post_author = cursor_line
if '□' in post_author:
post_author = post_author[:post_author.find('□')].strip()
elif 'R:' in post_author:
post_author = post_author[:post_author.find('R:')].strip()
elif ' 轉 ' in post_author:
post_author = post_author[:post_author.find('轉')].strip()
elif ' 鎖 ' in post_author:
post_author = post_author[:post_author.find('鎖')].strip()
lock_post = True
post_author = post_author[post_author.rfind(' '):].strip()
post_title = cursor_line
if ' □ ' in post_title:
post_title = post_title[post_title.find('□') + 1:].strip()
elif ' R:' in post_title:
post_title = post_title[post_title.find('R:'):].strip()
elif ' 轉 ' in post_title:
post_title = post_title[post_title.find('轉') + 1:].strip()
post_title = f'Fw: {post_title}'
elif ' 鎖 ' in post_title:
post_title = post_title[post_title.find('鎖') + 1:].strip()
ori_screen_temp = ori_screen[ori_screen.find('┌──'):]
ori_screen_temp = ori_screen_temp[:ori_screen_temp.find('└──')]
aid_line = [line for line in ori_screen.split(
'\n') if line.startswith('│ 文章代碼(AID)')]
post_aid = None
if len(aid_line) == 1:
aid_line = aid_line[0]
pattern = re.compile('#[\w|-]+')
pattern_result = pattern.search(aid_line)
post_aid = pattern_result.group(0)[1:]
pattern = re.compile('文章網址: https:[\S]+html')
pattern_result = pattern.search(ori_screen_temp)
if pattern_result is None:
post_web = None
else:
post_web = pattern_result.group(0)[6:]
pattern = re.compile('這一篇文章值 [\d]+ Ptt幣')
pattern_result = pattern.search(ori_screen_temp)
if pattern_result is None:
# 特殊文章無價格
post_money = -1
else:
post_money = pattern_result.group(0)[7:]
post_money = post_money[:post_money.find(' ')]
post_money = int(post_money)
pattern = re.compile('[\d]+\/[\d]+')
pattern_result = pattern.search(cursor_line)
if pattern_result is None:
list_date = None
else:
list_date = pattern_result.group(0)
list_date = list_date[-5:]
# print(list_date)
# > 7485 9 8/09 CodingMan □ [閒聊] PTT Library 更新
# > 79189 M 1 9/17 LittleCalf □ [公告] 禁言退文公告
# >781508 +爆 9/17 jodojeda □ [新聞] 國人吃魚少 學者:應把吃魚當成輕鬆愉快
# >781406 +X1 9/17 kingofage111 R: [申請] ReDmango 請辭Gossiping板主職務
pattern = re.compile('[\d]+')
pattern_result = pattern.search(cursor_line)
post_index = 0
if pattern_result is not None:
post_index = int(pattern_result.group(0))
push_number = cursor_line
push_number = push_number[7:11]
push_number = push_number.split(' ')
push_number = list(filter(None, push_number))
if len(push_number) == 0:
push_number = None
else:
push_number = push_number[-1]
if push_number.startswith('爆') or push_number.startswith('~爆'):
push_number = '爆'
if push_number.startswith('+') or push_number.startswith('~'):
push_number = push_number[1:]
if push_number.lower().startswith('m'):
push_number = push_number[1:]
if push_number.lower().startswith('!'):
push_number = push_number[1:]
if push_number.lower().startswith('s'):
push_number = push_number[1:]
if push_number.lower().startswith('='):
push_number = push_number[1:]
if len(push_number) == 0:
push_number = None
log.logger.debug('PostAuthor', post_author)
log.logger.debug('PostTitle', post_title)
log.logger.debug('PostAID', post_aid)
log.logger.debug('PostWeb', post_web)
log.logger.debug('PostMoney', post_money)
log.logger.debug('ListDate', list_date)
log.logger.debug('PushNumber', push_number)
return lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index
def get_search_condition_cmd(index_type: data_type.NewIndex, search_list: Optional[list] = None):
cmd_list = []
if not search_list:
return cmd_list
for search_type, search_condition in search_list:
if search_type == data_type.SearchType.KEYWORD:
cmd_list.append('/')
elif search_type == data_type.SearchType.AUTHOR:
cmd_list.append('a')
elif search_type == data_type.SearchType.MARK:
cmd_list.append('G')
elif index_type == data_type.NewIndex.BOARD:
if search_type == data_type.SearchType.COMMENT:
cmd_list.append('Z')
elif search_type == data_type.SearchType.MONEY:
cmd_list.append('A')
else:
continue
else:
continue
cmd_list.append(search_condition)
cmd_list.append(command.enter)
return cmd_list
def goto_board(api, board: str, refresh: bool = False, end: bool = False) -> None:
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('qs')
cmd_list.append(board)
cmd_list.append(command.enter)
cmd_list.append(command.space)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('任意鍵', log_level=log.DEBUG, response=' '),
connect_core.TargetUnit('互動式動畫播放中', log_level=log.DEBUG, response=command.ctrl_c),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
]
if refresh:
current_refresh = True
else:
if board.lower() in api._goto_board_list:
current_refresh = True
else:
current_refresh = False
api._goto_board_list.append(board.lower())
api.connect_core.send(cmd, target_list, refresh=current_refresh)
if end:
cmd_list = []
cmd_list.append('1')
cmd_list.append(command.enter)
cmd_list.append('$')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
]
api.connect_core.send(cmd, target_list)
def one_thread(api):
current_thread_id = threading.get_ident()
if current_thread_id != api._thread_id:
raise exceptions.MultiThreadOperated()
@functools.lru_cache(maxsize=64)
def check_board(api, board: str, check_moderator: bool = False) -> Dict:
if board.lower() not in api._exist_board_list:
board_info = _api_get_board_info.get_board_info(api, board, get_post_kind=False, call_by_others=False)
api._exist_board_list.append(board.lower())
api._board_info_list[board.lower()] = board_info
moderators = board_info[data_type.BoardField.moderators]
moderators = [x.lower() for x in moderators]
api._moderators[board.lower()] = moderators
api._board_info_list[board.lower()] = board_info
if check_moderator:
if api.ptt_id.lower() not in api._moderators[board.lower()]:
raise exceptions.NeedModeratorPermission(board)
return api._board_info_list[board.lower()]
================================================
FILE: PyPtt/check_value.py
================================================
from . import i18n
from . import log
def check_type(value, value_type, name) -> None:
if not isinstance(value, value_type):
if value_type is str:
raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_string}, but got {value}')
elif value_type is int:
raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_integer}, but got {value}')
elif value_type is bool:
raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_boolean}, but got {value}')
else:
raise TypeError(f'[PyPtt] {name} {i18n.must_be} {value_type}, but got {value}')
def check_range(value, min_value, max_value, name) -> None:
check_type(value, int, name)
check_type(min_value, int, 'min_value')
check_type(max_value, int, 'max_value')
if min_value <= value <= max_value:
return
raise ValueError(f'{name} {value} {i18n.must_between} {min_value} ~ {max_value}')
def check_index(name, index, max_value=None) -> None:
check_type(index, int, name)
if index < 1:
raise ValueError(f'{name} {i18n.must_bigger_than} 0')
if max_value is not None:
if index > max_value:
log.logger.info('index', index)
log.logger.info('max_value', max_value)
raise ValueError(f'{name} {index} {i18n.must_between} 0 ~ {max_value}')
def check_index_range(start_name, start_index, end_name, end_index, max_value=None) -> None:
check_type(start_index, int, start_name)
check_type(end_index, int, end_name)
if start_index < 1:
raise ValueError(f'{start_name} {start_index} {i18n.must_bigger_than} 0')
if end_index <= 1:
raise ValueError(f'{end_name} {end_index} {i18n.must_bigger_than} 1')
if start_index > end_index:
raise ValueError(f'{end_name} {end_index} {i18n.must_bigger_than} {start_name} {start_index}')
if max_value is not None:
if start_index > max_value:
raise ValueError(f'{start_name} {start_index} {i18n.must_small_than} {max_value}')
if end_index > max_value:
raise ValueError(f'{end_name} {end_index} {i18n.must_small_than} {max_value}')
if __name__ == '__main__':
QQ = str
if QQ is str:
print('1')
if QQ == str:
print('2')
if isinstance('', QQ):
print('3')
================================================
FILE: PyPtt/command.py
================================================
# http://www.physics.udel.edu/~watson/scen103/ascii.html
enter = '\r'
tab = '\t'
ctrl_c = '\x03'
ctrl_d = '\x04'
ctrl_e = '\x05'
ctrl_f = '\x06'
ctrl_h = '\x08'
ctrl_l = '\x0C'
ctrl_p = '\x10'
ctrl_u = '\x15'
ctrl_x = '\x18'
ctrl_y = '\x19'
ctrl_z = '\x1A'
star = '\x2A'
up = '\x1b\x4fA'
down = '\x1b\x4fB'
right = '\x1b\x4fC'
left = '\x1b\x4fD'
space = ' '
query_post = 'Q'
comment = 'X'
go_main_menu = ' ' + left * 5
go_main_menu_type_q = 'q' * 5
refresh = ctrl_l
control_code = ctrl_u + star
backspace = ctrl_h
================================================
FILE: PyPtt/config.py
================================================
from . import data_type
from . import log
class Config:
# retry_wait_time 秒後重新連線
retry_wait_time = 3
# ScreenLTimeOut 秒後判定此畫面沒有可辨識的目標
screen_timeout = 3.0
# screen_long_timeout 秒後判定此畫面沒有可辨識的目標
# 適用於需要特別等待的情況,例如: 剔除其他登入等等
# 建議不要低於 10 秒,剔除其他登入最長可能會花費約六到七秒
screen_long_timeout = 10.0
# screen_post_timeout 秒後判定此畫面沒有可辨識的目標
# 適用於貼文等待的情況,建議不要低於 60 秒
screen_post_timeout = 60.0
# 預設語言
language = data_type.Language.MANDARIN
# 預設 log 等級
log_level = log.INFO
# 預設不剔除其他登入
kick_other_session = False
# 預設登入 PTT1
host = data_type.HOST.PTT1
# 預設採用 websockets
connect_mode = None
# 預設使用 23
port = 23
logger_callback = None
LOGGER_CONFIG = {
}
================================================
FILE: PyPtt/connect_core.py
================================================
from __future__ import annotations
import asyncio
import ssl
import telnetlib
import threading
import time
import traceback
import warnings
from typing import Any
import websockets
import websockets.exceptions
import websockets.http
import PyPtt
from . import command
from . import data_type
from . import exceptions
from . import i18n
from . import log
from . import screens
websockets.http.USER_AGENT += f' PyPtt/{PyPtt.__version__}'
ssl_context = ssl.create_default_context()
class TargetUnit:
def __init__(self, detect_target, log_level: log.LogLevel = None, response: [Any | str] = '', break_detect=False,
break_detect_after_send=False, exceptions_=None, refresh=True, secret=False, handler=None,
max_match: int = 0):
self.detect_target = detect_target
if log_level is None:
self.log_level = log.INFO
else:
self.log_level = log_level
self._response_func = response
self._break_detect = break_detect
self._exception = exceptions_
self._refresh = refresh
self._break_after_send = break_detect_after_send
self._secret = secret
self._Handler = handler
self._max_match = max_match
self._current_match = 0
def is_match(self, screen: str) -> bool:
if self._current_match >= self._max_match > 0:
return False
if isinstance(self.detect_target, str):
if self.detect_target in screen:
self._current_match += 1
return True
return False
elif isinstance(self.detect_target, list):
for Target in self.detect_target:
if Target not in screen:
return False
self._current_match += 1
return True
def get_detect_target(self):
return self.detect_target
def get_log_level(self):
return self.log_level
def get_response(self, screen: str) -> str:
if callable(self._response_func):
return self._response_func(screen)
return self._response_func
def is_break(self) -> bool:
return self._break_detect
def raise_exception(self):
if isinstance(self._exception, Exception):
raise self._exception
def is_refresh(self) -> bool:
return self._refresh
def is_break_after_send(self) -> bool:
return self._break_after_send
def is_secret(self) -> bool:
return self._secret
class RecvData:
def __init__(self):
self.data = None
async def websocket_recv_func(core, recv_data_obj):
recv_data_obj.data = await core.recv()
async def websocket_receiver(core, screen_timeout, recv_data_obj):
# Wait for at most 1 second
await asyncio.wait_for(
websocket_recv_func(core, recv_data_obj),
timeout=screen_timeout)
class ReceiveDataQueue(object):
def __init__(self):
self._ReceiveDataQueue = []
def add(self, screen):
self._ReceiveDataQueue.append(screen)
self._ReceiveDataQueue = self._ReceiveDataQueue[-10:]
def get(self, last=1):
return self._ReceiveDataQueue[-last:]
class API(object):
def __init__(self, config):
self.current_encoding = 'big5uao'
self.config = config
self._RDQ = ReceiveDataQueue()
self._UseTooManyResources = TargetUnit(screens.Target.use_too_many_resources,
exceptions_=exceptions.UseTooManyResources())
def connect(self) -> None:
def _wait():
for i in range(self.config.retry_wait_time):
if self.config.host == data_type.HOST.PTT1:
log.logger.info(i18n.prepare_connect_again, i18n.PTT, str(self.config.retry_wait_time - i))
elif self.config.host == data_type.HOST.PTT2:
log.logger.info(i18n.prepare_connect_again, i18n.PTT2, str(self.config.retry_wait_time - i))
elif self.config.host == data_type.HOST.LOCALHOST:
log.logger.info(i18n.prepare_connect_again, i18n.localhost, str(self.config.retry_wait_time - i))
else:
log.logger.info(i18n.prepare_connect_again, self.config.host, str(self.config.retry_wait_time - i))
time.sleep(1)
warnings.filterwarnings("ignore", category=DeprecationWarning)
self.current_encoding = 'big5uao'
# self.log.py.info(i18n.connect_core, i18n.active)
if self.config.host == data_type.HOST.PTT1:
telnet_host = 'ptt.cc'
websocket_host = 'wss://ws.ptt.cc/bbs/'
websocket_origin = 'https://term.ptt.cc'
elif self.config.host == data_type.HOST.PTT2:
telnet_host = 'ptt2.cc'
websocket_host = 'wss://ws.ptt2.cc/bbs/'
websocket_origin = 'https://term.ptt2.cc'
elif self.config.host == data_type.HOST.LOCALHOST:
telnet_host = 'localhost'
websocket_host = 'wss://localhost'
websocket_origin = 'https://term.ptt.cc'
else:
telnet_host = self.config.host
websocket_host = f'wss://{self.config.host}'
websocket_origin = 'https://term.ptt.cc'
connect_success = False
for _ in range(2):
try:
if self.config.connect_mode == data_type.ConnectMode.TELNET:
self._core = telnetlib.Telnet(telnet_host, self.config.port)
else:
if not threading.current_thread() is threading.main_thread():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
log.logger.debug('USER_AGENT', websockets.http.USER_AGENT)
self._core = asyncio.get_event_loop().run_until_complete(
websockets.connect(
websocket_host,
origin=websocket_origin,
ssl=ssl_context))
connect_success = True
except Exception as e:
traceback.print_tb(e.__traceback__)
print(e)
if self.config.host == data_type.HOST.PTT1:
log.logger.info(i18n.connect, i18n.PTT, i18n.fail)
elif self.config.host == data_type.HOST.PTT2:
log.logger.info(i18n.connect, i18n.PTT2, i18n.fail)
elif self.config.host == data_type.HOST.LOCALHOST:
log.logger.info(i18n.connect, i18n.localhost, i18n.fail)
else:
log.logger.info(i18n.connect, self.config.host, i18n.fail)
_wait()
continue
break
if not connect_success:
raise exceptions.ConnectError(self.config)
def _decode_screen(self, receive_data_buffer, start_time, target_list, is_secret, refresh, msg):
break_detect_after_send = False
use_too_many_res = False
vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding)
screen = vt100_p.screen
find_target = False
target_index = -1
for target in target_list:
condition = target.is_match(screen)
if condition:
if target._Handler is not None:
target._Handler(screen)
if len(screen) > 0:
screens.show(self.config, screen)
self._RDQ.add(screen)
if target == self._UseTooManyResources:
use_too_many_res = True
# print(f'1 {use_too_many_res}')
break
target.raise_exception()
find_target = True
end_time = time.time()
log.logger.debug(i18n.spend_time, round(end_time - start_time, 3))
if target.is_break():
target_index = target_list.index(target)
break
msg = target.get_response(screen)
add_refresh = False
if target.is_refresh():
add_refresh = True
elif refresh:
add_refresh = True
if add_refresh:
if not msg.endswith(command.refresh):
msg = msg + command.refresh
is_secret = target.is_secret()
if target.is_break_after_send():
# break_index = target_list.index(target)
break_detect_after_send = True
break
return screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index
def send(self, msg: str, target_list: list, screen_timeout: int = 0, refresh: bool = True,
secret: bool = False) -> int:
if not all(isinstance(T, TargetUnit) for T in target_list):
raise ValueError('Item of TargetList must be TargetUnit')
if self._UseTooManyResources not in target_list:
target_list.append(self._UseTooManyResources)
if screen_timeout == 0:
current_screen_timeout = self.config.screen_timeout
else:
current_screen_timeout = screen_timeout
break_detect_after_send = False
is_secret = secret
use_too_many_res = False
while True:
if refresh and not msg.endswith(command.refresh):
msg = msg + command.refresh
try:
msg = msg.encode('utf-8', 'replace')
except AttributeError:
pass
except Exception as e:
traceback.print_tb(e.__traceback__)
print(e)
msg = msg.encode('utf-8', 'replace')
if is_secret:
log.logger.debug(i18n.send_msg, i18n.hide_sensitive_info)
else:
log.logger.debug(i18n.send_msg, str(msg))
if self.config.connect_mode == data_type.ConnectMode.TELNET:
try:
self._core.read_very_eager()
self._core.write(msg)
except EOFError:
raise exceptions.ConnectionClosed()
else:
try:
asyncio.get_event_loop().run_until_complete(
self._core.send(msg))
except websockets.exceptions.ConnectionClosedError:
raise exceptions.ConnectionClosed()
except RuntimeError:
raise exceptions.ConnectionClosed()
except websockets.exceptions.ConnectionClosedOK:
raise exceptions.ConnectionClosed()
if break_detect_after_send:
return -1
msg = ''
receive_data_buffer = bytes()
start_time = time.time()
mid_time = time.time()
while mid_time - start_time < current_screen_timeout:
# print(1)
recv_data_obj = RecvData()
if self.config.connect_mode == data_type.ConnectMode.TELNET:
try:
recv_data_obj.data = self._core.read_very_eager()
except EOFError:
return -1
else:
try:
asyncio.get_event_loop().run_until_complete(
websocket_receiver(
self._core, current_screen_timeout, recv_data_obj))
except websockets.exceptions.ConnectionClosed:
if use_too_many_res:
raise exceptions.UseTooManyResources()
raise exceptions.ConnectionClosed()
except websockets.exceptions.ConnectionClosedOK:
raise exceptions.ConnectionClosed()
except asyncio.TimeoutError:
return -1
except RuntimeError:
raise exceptions.ConnectionClosed()
receive_data_buffer += recv_data_obj.data
screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)
if self.current_encoding == 'big5uao' and not find_target:
self.current_encoding = 'utf-8'
screen_, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)
if find_target:
screen = screen_
else:
self.current_encoding = 'big5uao'
# print(4)
if target_index != -1:
return target_index
if use_too_many_res:
continue
if find_target:
break
if len(screen) > 0:
screens.show(self.config, screen)
self._RDQ.add(screen)
# print(6)
mid_time = time.time()
if not find_target:
return -1
return -2
def close(self):
if self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:
asyncio.get_event_loop().run_until_complete(self._core.close())
else:
self._core.close()
def get_screen_queue(self) -> list:
return self._RDQ.get(1)
================================================
FILE: PyPtt/data_type.py
================================================
import time
from enum import auto
from AutoStrEnum import AutoStrEnum
class Language:
MANDARIN = 'zh_TW'
ENGLISH = 'en_US'
class ConnectMode(AutoStrEnum):
TELNET = auto()
WEBSOCKETS = auto()
class SearchType(AutoStrEnum):
"""文章搜尋類型"""
NOPE = auto()
# 搜尋關鍵字 / ?
KEYWORD = auto()
# 搜尋作者 a
AUTHOR = auto()
# 搜尋推文數 Z
COMMENT = auto()
# 搜尋標記 G
MARK = auto()
# 搜尋稿酬 A
MONEY = auto()
class ReplyTo(AutoStrEnum):
# 回文類型
BOARD = auto()
MAIL = auto()
BOARD_MAIL = auto()
class CommentType(AutoStrEnum):
PUSH = auto()
BOO = auto()
ARROW = auto()
class UserField(AutoStrEnum):
ptt_id = auto()
money = auto()
login_count = auto()
account_verified = auto()
legal_post = auto()
illegal_post = auto()
activity = auto()
mail = auto()
last_login_date = auto()
last_login_ip = auto()
five_chess = auto()
chess = auto()
signature_file = auto()
class CommentField(AutoStrEnum):
type = auto()
author = auto()
content = auto()
ip = auto()
time = auto()
class PostStatus(AutoStrEnum):
EXISTS = auto()
DELETED_BY_AUTHOR = auto()
DELETED_BY_MODERATOR = auto()
DELETED_BY_UNKNOWN = auto()
class PostField(AutoStrEnum):
board = auto()
aid = auto()
index = auto()
author = auto()
date = auto()
title = auto()
content = auto()
money = auto()
url = auto()
ip = auto()
comments = auto()
post_status = auto()
list_date = auto()
has_control_code = auto()
pass_format_check = auto()
location = auto()
push_number = auto()
is_lock = auto()
full_content = auto()
is_unconfirmed = auto()
# class WaterballInfo:
# def __init__(self, waterball_type, target, content, date):
# self.type: int = parse_para(int, waterball_type)
# self.target: str = parse_para(str, target)
# self.content: str = parse_para(str, content)
# self.date: str = parse_para(str, date)
class Cursor:
# 舊式游標
OLD: str = '●'
# 新式游標
NEW: str = '>'
class NewIndex(AutoStrEnum):
# 看板
BOARD = auto()
# 信箱
MAIL = auto()
# 網頁,尚不支援
# WEB = auto()
class HOST(AutoStrEnum):
# 批踢踢萬
PTT1 = auto()
# 批踢踢兔
PTT2 = auto()
# 本機測試用
LOCALHOST = auto()
class MarkType(AutoStrEnum):
# s 文章
S = auto()
# 標記文章
D = auto()
# 刪除標記文章
DELETE_D = auto()
# M 起來
M = auto()
# 待證實文章
UNCONFIRMED = auto()
class FavouriteBoardField(AutoStrEnum):
board = auto()
type = auto()
title = auto()
class MailField(AutoStrEnum):
origin_mail = auto()
author = auto()
title = auto()
date = auto()
content = auto()
ip = auto()
location = auto()
is_red_envelope = auto()
class BoardField(AutoStrEnum):
board = auto()
online_user = auto()
mandarin_des = auto()
moderators = auto()
open_status = auto()
into_top_ten_when_hide = auto()
can_non_board_members_post = auto()
can_reply_post = auto()
self_del_post = auto()
can_comment_post = auto()
can_boo_post = auto()
can_fast_push = auto()
min_interval_between_comments = auto()
is_comment_record_ip = auto()
is_comment_aligned = auto()
can_moderators_del_illegal_content = auto()
does_tran_post_auto_recorded_and_require_post_permissions = auto()
is_cool_mode = auto()
is_require18 = auto()
require_login_time = auto()
require_illegal_post = auto()
# post_kind = auto()
post_kind_list = auto()
class Compare(AutoStrEnum):
BIGGER = auto()
SAME = auto()
SMALLER = auto()
UNKNOWN = auto()
class TimedDict:
def __init__(self, timeout: int = 0):
self.timeout = timeout
self.data = {}
self.timestamps = {}
def __setitem__(self, key, value):
self.data[key] = value
self.timestamps[key] = time.time()
def __getitem__(self, key):
if key not in self.data:
raise KeyError(key)
timestamp = self.timestamps[key]
if time.time() - timestamp > self.timeout > 0:
del self.data[key]
del self.timestamps[key]
raise KeyError(key)
return self.data[key]
def __contains__(self, key):
try:
self[key]
except KeyError:
return False
else:
return True
def __len__(self):
self.cleanup()
return len(self.data)
def cleanup(self):
if self.timeout == 0:
return
now = time.time()
to_remove = [key for key, timestamp in self.timestamps.items()
if now - timestamp > self.timeout > 0]
for key in to_remove:
del self.data[key]
del self.timestamps[key]
================================================
FILE: PyPtt/exceptions.py
================================================
from . import data_type
from . import i18n
class Error(Exception):
pass
class UnknownError(Error):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
class RequireLogin(Error):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
class NoPermission(Error):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
class LoginError(Error):
def __init__(self):
self.message = i18n.login_fail
def __str__(self):
return self.message
class NoFastComment(Error):
def __init__(self):
self.message = i18n.no_fast_comment
def __str__(self):
return self.message
class NoSuchUser(Error):
def __init__(self, user):
self.message = i18n.no_such_user + ': ' + user
def __str__(self):
return self.message
class NoSuchMail(Error):
def __init__(self):
self.message = i18n.no_such_mail
def __str__(self):
return self.message
# class UserOffline(Error):
# def __init__(self, user):
# self.message = i18n.user_offline + ': ' + user
#
# def __str__(self):
# return self.message
# class ParseError(Error):
# def __init__(self, screen):
# self.message = screen
#
# def __str__(self):
# return self.message
class NoMoney(Error):
def __init__(self):
self.message = i18n.no_money
def __str__(self):
return self.message
class NoSuchBoard(Error):
def __init__(self, config, board):
if config.host == data_type.HOST.PTT1:
self.message = [
i18n.PTT,
i18n.no_such_board
]
else:
self.message = [
i18n.PTT2,
i18n.no_such_board
]
if config.language == data_type.Language.MANDARIN:
self.message = ''.join(self.message) + ': ' + board
else:
self.message = ' '.join(self.message) + ': ' + board
def __str__(self):
return self.message
class ConnectionClosed(Error):
def __init__(self):
self.message = i18n.connection_closed
def __str__(self):
return self.message
class UnregisteredUser(Error):
def __init__(self, api_name):
self.message = i18n.unregistered_user_cant_use_this_api + ': ' + api_name
def __str__(self):
return self.message
class MultiThreadOperated(Error):
def __init__(self):
self.message = i18n.multi_thread_operate
def __str__(self):
return self.message
class WrongIDorPassword(Error):
def __init__(self):
self.message = i18n.wrong_id_pw
def __str__(self):
return self.message
class WrongPassword(Error):
def __init__(self):
self.message = i18n.error_pw
def __str__(self):
return self.message
class LoginTooOften(Error):
def __init__(self):
self.message = i18n.login_too_often
def __str__(self):
return self.message
class UseTooManyResources(Error):
def __init__(self):
self.message = i18n.use_too_many_resources
def __str__(self):
return self.message
class HostNotSupport(Error):
def __init__(self, api):
self.message = f'{i18n.ptt2_not_support}: {api}'
def __str__(self):
return self.message
class CantComment(Error):
def __init__(self):
self.message = i18n.no_comment
def __str__(self):
return self.message
class CantResponse(Error):
def __init__(self):
self.message = i18n.no_response
def __str__(self):
return self.message
class NeedModeratorPermission(Error):
def __init__(self, board):
self.message = f'{i18n.need_moderator_permission}: {board}'
def __str__(self):
return self.message
class ConnectError(Error):
def __init__(self, config):
self.message = i18n.connect_fail
def __str__(self):
return self.message
class NoSuchPost(Error):
def __init__(self, board, aid):
self.message = i18n.replace(
i18n.no_such_post,
board,
aid)
def __str__(self):
return self.message
class CanNotUseSearchPostCode(Error):
"""
此狀態下無法使用搜尋文章代碼(AID)功能
"""
def __init__(self):
self.message = i18n.can_not_use_search_post_code_f
def __str__(self):
return self.message
class UserHasPreviouslyBeenBanned(Error):
def __init__(self):
self.message = i18n.user_has_previously_been_banned
def __str__(self):
return self.message
class MailboxFull(Error):
def __init__(self):
self.message = i18n.mail_box_full
def __str__(self):
return self.message
class NoSearchResult(Error):
def __init__(self):
self.message = i18n.no_search_result
def __str__(self):
return self.message
# 此帳號已設定為只能使用安全連線
class OnlySecureConnection(Error):
def __init__(self):
self.message = i18n.only_secure_connection
def __str__(self):
return self.message
class SetContactMailFirst(Error):
def __init__(self):
self.message = i18n.set_contact_mail_first
def __str__(self):
return self.message
class ResetYourContactEmail(Error):
def __init__(self):
self.message = i18n.reset_your_contact_email
def __str__(self):
return self.message
================================================
FILE: PyPtt/i18n.py
================================================
import os
import random
import yaml
from . import __version__
from . import data_type
locale_pool = {
data_type.Language.ENGLISH,
data_type.Language.MANDARIN
}
_script_path = os.path.dirname(os.path.abspath(__file__))
_lang_data = {}
mapping = {
'{version}': __version__,
}
def replace(string, *args):
for i in range(len(args)):
target = f'{args[i]}'
string = string.replace(f'_target{i}_', target)
return string
def init(locale: str, cache: bool = False) -> None:
if locale not in locale_pool:
raise ValueError(f'Unknown locale: {locale}')
language_file = f'{_script_path}/lang/{locale}.yaml'
if not os.path.exists(language_file):
raise ValueError(f'Unknown locale file: {language_file}')
with open(language_file, "r") as f:
string_data = yaml.safe_load(f)
for k, v in string_data.items():
if isinstance(v, list):
v = random.choice(v)
elif isinstance(v, str):
pass
else:
raise ValueError(f'Unknown string data type: {v}')
if locale == data_type.Language.ENGLISH:
v = v[0].upper() + v[1:]
for mk, mv in mapping.items():
v = v.replace(mk, mv)
globals()[k] = v
if cache:
global _lang_data
_lang_data[k] = v
================================================
FILE: PyPtt/lang/en_US.yaml
================================================
PTT: PTT
PTT2: PTT2
active: Active
author: Author
board: Board
can_not_use_search_post_code_f: This status can not use the search PostField code function
catch_bottom_post_success: Catch bottom post success
change_pw: Change password
comment: Comment
comment_content: Comment content
comment_date: Comment date
comment_id: Comment id
connect: Connect
connect_core: Connect core
connect_fail: Connect fail
connect_mode_TELNET: Telnet
connect_mode_WEBSOCKET: WebSocket
connection_closed: Connection Closed
content: Content
current_version: Current version
date: Date
delete_post: Delete post
development_version: Running development version
done: Done
english_module: English
error_pw: Wrong password
fail: Fail
find_newest_index: Find newest index
get_board_info: Get board info _target0_
get_favourite_board_list: Query favourite board list
get_mail: Get mail
give_money_to: give _target0_ _target1_ P coins
goodbye:
- good bye
- bye
- see you
- catch you later
- I hate to run, but...
- Until we meet again, I will wait
has_comment_permission: User has permission to comment
has_new_mail_goto_main_menu: New mail! Back to main menu
has_post_permission: Have permission to post
hide_sensitive_info: Hide sensitive info
in_login_process_please_wait: In login process, please wait
initialization: Init
kick_other_login: Kick other login
latest_version: Running the latest version
localhost: Localhost
login_fail: Login fail
login_id: Login id
login_success: Login ... success
login_too_often: Login too often
logout: Logout
mail_box_full: Mail box is full
mandarin_module: Mandarin
multi_thread_operate: Do not use a multi-thread to operate a PyPtt object
must_be: Must be
must_be_a_boolean: Must be a boolean
must_be_a_integer: Must be a integer
must_be_a_string: Must be a string
must_between: Must between
must_bigger_than: Must bigger than
must_small_than: Must smaller than
need_moderator_permission: Need moderator permission
new_cursor: New cursor
new_version: There is a new version
no_comment: No comment
no_fast_comment: No fast comment
no_money: Not enough PTT coins
no_permission: User Has No Permission
no_response: This post has been closed and marked, no response
no_search_result: No Search Result
no_such_board: No such board
no_such_mail: No such mail
no_such_post: In _target0_, the post code is not EXISTS _target1_
no_such_user: No such user
not_kick_other_login: Not kick other login
not_push_aligned: No push aligned
not_record_ip: Not record IP
old_cursor: Old cursor
only_secure_connection: Skip registration form
picks_in_register: Registration application processing order
post: Post article
post_deleted: Post has been deleted
prepare_connect_again: Prepare connect again
ptt2_not_support: PTT2 not support this api
push_aligned: Push aligned
record_ip: Record ip
reply_board: Respond to the BoardField
reply_board_mail: Respond to the board and the mailbox of author
reply_mail: Respond to the mailbox of author
require_login: Please login first
reset_your_contact_email: Please reset your contact email
retry: Retry
search_user: Search user
send_mail: Send mail
send_msg: Send msg
set_connect_host: Set up the connect host
set_connect_mode: Set up the connect mode
set_contact_mail_first: Password can only be changed after setting the contact mailbox
set_up_lang_module: Set up language module
spend_time: Spend time
substandard_post: Substandard post
success: Success
title: Title
transaction_cancelled: The transaction is cancelled!
unregistered_user_cant_use_all_api: Unregistered UserField Can't Use All API
unregistered_user_cant_use_this_api: Unregistered UserField Can't Use This API
update_remote_version: Fetching latest version
use_mailbox_api_will_logout_after_execution: If you use mailbox related functions, you will be logged out automatically after execution
use_too_many_resources: Use too many resources
user_has_previously_been_banned: User has previously been banned
user_offline: User offline
wait_for_no_fast_comment: Because no fast comment, wait 5 sec
welcome: PyPtt v _target0_ developed by CodingMan
wrong_id_pw: Wrong id or pw
================================================
FILE: PyPtt/lang/zh_TW.yaml
================================================
PTT: 批踢踢
PTT2: 批踢踢兔
active: 啟動
author: 作者
board: 看板
can_not_use_search_post_code_f: 此狀態下無法使用搜尋文章代碼(AID)功能
catch_bottom_post: 取得置底文章
change_pw: 變更密碼
comment: 推文
comment_content: 推文內文
comment_date: 推文日期
comment_id: 推文帳號
connect: 連線
connect_core: 連線核心
connect_fail: 連線失敗
connect_mode_TELNET: Telnet
connect_mode_WEBSOCKET: WebSocket
connection_closed: 連線已經被關閉
content: 內文
current_version: 目前版本
date: 日期
delete_post: 刪除文章
development_version: 正在執行開發版本
done: 完成
english_module: 英文
error_pw: 密碼不正確
fail: 失敗
find_newest_index: 找到最新編號
get_board_info: 取得看板資訊 _target0_
get_favourite_board_list: 取得我的最愛
get_mail: 取得信件
give_money_to: 給 _target0_ _target1_ P 幣
goodbye:
- 再見
- 下次再見
- 再會
- 祝平安
- 謝謝你,我很開心
- 我們會再見面的
has_comment_permission: 確認擁有推文權限
has_new_mail_goto_main_menu: 有新信,回到主選單
has_post_permission: 確認擁有貼文權限
hide_sensitive_info: 隱藏敏感資訊
in_login_process_please_wait: 登入中,請稍候
initialization: 初始化
kick_other_login: 強制執行剔除其他登入
latest_version: 正在執行最新版本
localhost: 本機
login_fail: 登入失敗
login_id: 登入帳號
login_success: 登入 ... 成功
login_too_often: 登入太頻繁
logout: 登出
mail_box_full: 郵件已滿
mandarin_module: 繁體中文
multi_thread_operate: 請勿使用多線程同時操作一個 PyPtt 物件
must_be: 必須為
must_be_a_boolean: 必須為布林值
must_be_a_integer: 必須為數字
must_be_a_string: 必須為字串
must_between: 必須介於
must_bigger_than: 必須大於
must_small_than: 必須小於
need_moderator_permission: 需要板主權限
new_cursor: 新式游標
new_version: 有新版本
no_comment: 禁止推薦
no_fast_comment: 禁止快速連續推文
no_money: PTT 幣不足
no_permission: 使用者沒有權限
no_response: 很抱歉, 此文章已結案並標記, 不得回應
no_search_result: 沒有搜尋結果
no_such_board: 無該看板
no_such_mail: 無此信件
no_such_post: _target0_ 板找不到這個文章代碼 _target1_
no_such_user: 無該使用者
not_kick_other_login: 不剔除其他登入
not_push_aligned: 無推文對齊
not_record_ip: 不紀錄 IP
old_cursor: 舊式游標
only_secure_connection: 跳過填寫註冊單
picks_in_register: 註冊申請單處理順位
post: 發佈文章
post_deleted: 文章已經被刪除
prepare_connect_again: 準備再次連線
ptt2_not_support: 批踢踢兔不支援此功能
push_aligned: 推文對齊
record_ip: 紀錄 IP
reply_board: 回應至看板
reply_board_mail: 回應至看板與作者信箱
reply_mail: 回應至作者信箱
require_login: 請先登入
reset_your_contact_email: 請重新設定您的聯絡信箱
retry: 重試
search_user: 搜尋使用者
send_mail: 寄信
send_msg: 送出訊息
set_connect_host: 設定連線主機
set_connect_mode: 設定連線模式
set_contact_mail_first: 請先設定聯絡信箱後才能修改密碼
set_up_lang_module: 設定語言模組
spend_time: 花費時間
substandard_post: 不合規範文章
success: 成功
title: 標題
transaction_cancelled: 交易取消!
unregistered_user_cant_use_all_api: 未註冊使用者,將無法使用全部功能
unregistered_user_cant_use_this_api: 未註冊使用者,無法使用此功能
update_remote_version: 確認最新版本
use_mailbox_api_will_logout_after_execution: 如果使用信箱相關功能,將執行後自動登出
use_too_many_resources: 耗用過多資源
user_has_previously_been_banned: 使用者之前已被禁言
user_offline: 使用者離線
wait_for_no_fast_comment: 因禁止快速連續推文,所以等待五秒
welcome: PyPtt v _target0_ 由 CodingMan 開發
wrong_id_pw: 帳號密碼錯誤
================================================
FILE: PyPtt/lib_util.py
================================================
import functools
import os
import random
import re
import string
import time
import traceback
from typing import Tuple
import requests
from . import __version__
from . import check_value
from . import data_type
from . import i18n
from . import log
def get_file_name(path_str: str) -> str:
result = os.path.basename(path_str)
result = result[:result.find('.')]
return result
def get_current_func_name() -> str:
return traceback.extract_stack(None, 2)[0][2]
def findnth(haystack, needle, n) -> int:
parts = haystack.split(needle, n + 1)
if len(parts) <= n + 1:
return -1
return len(haystack) - len(parts[-1]) - len(needle)
def get_random_str(length) -> str:
return ''.join(random.choices(string.hexdigits, k=length))
# 演算法參考 https://www.ptt.cc/man/C_Chat/DE98/DFF5/DB61/M.1419434423.A.DF0.html
# aid 字元表
aid_table = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'
def get_aid_from_url(url: str) -> Tuple[str, str]:
# 檢查是否為字串
check_value.check_type(url, str, 'url')
# 檢查是否符合 PTT BOARD 文章網址格式
pattern = re.compile('https://www.ptt.cc/bbs/[-.\w]+/M.[\d]+.A[.\w]*.html')
r = pattern.search(url)
if r is None:
raise ValueError('wrong parameter url must be www.ptt.cc post url')
board = url[23:]
board = board[:board.find('/')]
temp = url[url.rfind('/') + 1:].split('.')
# print(temp)
id_0 = int(temp[1]) # dec
aid_0 = ''
for _ in range(6):
index = id_0 % 64
aid_0 = f'{aid_table[index]}{aid_0}'
id_0 = int(id_0 / 64)
if temp[3] != 'html':
id_1 = int(temp[3], 16) # hex
aid_1 = ''
for _ in range(2):
index = id_1 % 64
aid_1 = f'{aid_table[index]}{aid_1}'
id_1 = int(id_1 / 64)
else:
aid_1 = '00'
aid = f'{aid_0}{aid_1}'
return board, aid
sync_version_compare: data_type.Compare = data_type.Compare.UNKNOWN
sync_version_result: str = ''
def sync_version() -> Tuple[data_type.Compare, str]:
global sync_version_compare
global sync_version_result
if sync_version_compare is not data_type.Compare.UNKNOWN:
return sync_version_compare, sync_version_result
log.logger.info(i18n.update_remote_version)
r = None
for i in range(3):
try:
r = requests.get(
'https://raw.githubusercontent.com/PyPtt/PyPtt/master/PyPtt/__init__.py',
timeout=3)
break
except requests.exceptions.ReadTimeout:
log.logger.info(i18n.retry)
time.sleep(0.5)
if r is None:
log.logger.info(i18n.update_remote_version, i18n.fail)
return data_type.Compare.SAME, ''
log.logger.info(i18n.update_remote_version, i18n.success)
text = r.text
remote_version = [line for line in text.split('\n') if line.startswith('__version__')][0]
remote_version = remote_version[remote_version.find("'") + 1:]
remote_version = remote_version[:remote_version.find("'")]
current_version = __version__
if 'dev' in current_version:
current_version = current_version[:current_version.find('dev') - 1]
version_list = [int(v) for v in current_version.split('.')]
remote_version_list = [int(v) for v in remote_version.split('.')]
sync_version_compare = data_type.Compare.SAME
for i in range(len(version_list)):
if remote_version_list[i] < version_list[i]:
sync_version_compare = data_type.Compare.BIGGER
break
if version_list[i] < remote_version_list[i]:
sync_version_compare = data_type.Compare.SMALLER
break
return sync_version_compare, remote_version
def uniform_new_line(text: str) -> str:
random_tag = get_random_str(10)
text = text.replace('\r\n', random_tag)
text = text.replace('\n', '\r\n')
text = text.replace(random_tag, '\r\n')
return text
@functools.lru_cache(maxsize=64)
def check_aid(aid: str) -> str:
if aid is None:
raise ValueError('aid is None')
if not isinstance(aid, str):
raise TypeError('aid is not str')
if aid.startswith('#'):
aid = aid[1:]
if len(aid) != 8:
raise ValueError('aid is not valid')
# check the char of aid is in aid_table or not
for char in aid:
if char not in aid_table:
raise ValueError('aid is not valid')
return f'#{aid}'
if __name__ == '__main__':
check_aid('#1aBzRW4z')
================================================
FILE: PyPtt/log.py
================================================
import logging
from typing import Optional
class LogLv:
_level: int
def __init__(self, level):
self._level = level
@property
def level(self):
return self._level
def __eq__(self, other):
return self.level == other.level
SILENT = LogLv(logging.NOTSET)
INFO = LogLv(logging.INFO)
DEBUG = LogLv(logging.DEBUG)
# deprecated use DEBUG instead
TRACE = DEBUG
class LogLevel:
SILENT = LogLv(logging.NOTSET)
INFO = LogLv(logging.INFO)
DEBUG = LogLv(logging.DEBUG)
TRACE = DEBUG
_logger_pool = {}
_console_handler = logging.StreamHandler()
_console_handler.setFormatter(logging.Formatter(
fmt='[%(asctime)s][%(name)s][%(levelname)s] %(message)s',
datefmt='%m.%d %H:%M:%S'))
def _combine_msg(*args) -> str:
"""
將多個字串組合成一個字串。
Args:
args: 要組合的字串。
Returns:
組合後的字串。
"""
if not args:
return ''
msg = list(map(str, args))
msg[0] = msg[0][0].upper() + msg[0][1:]
return ' '.join(msg)
class Logger:
logger: logging.Logger
def __init__(self, name: str, level: int = logging.NOTSET, logger_callback: Optional[callable] = None):
self.logger = logging.getLogger(name)
self.logger.setLevel(level)
if self.logger.hasHandlers():
for handler in self.logger.handlers:
handler.setFormatter(_console_handler.formatter)
else:
self.logger.addHandler(_console_handler)
self.logger_callback: Optional[callable] = None
if logger_callback and callable(logger_callback):
self.logger_callback = logger_callback
def info(self, *args):
if not self.logger.isEnabledFor(logging.INFO):
return
msg = _combine_msg(*args)
self.logger.info(msg)
if self.logger_callback:
self.logger_callback(msg)
def debug(self, *args):
if not self.logger.isEnabledFor(logging.DEBUG):
return
msg = _combine_msg(*args)
self.logger.debug(msg)
if self.logger_callback:
self.logger_callback(msg)
logger: Optional[Logger] = None
def init(log_level: LogLv, name: Optional[str] = None, logger_callback: Optional[callable] = None) -> Logger:
name = name or 'PyPtt'
current_logger = Logger(name, level=log_level.level, logger_callback=logger_callback)
if name == 'PyPtt':
global logger
logger = current_logger
return current_logger
if __name__ == '__main__':
logger = init(INFO)
logger.info('1')
logger.info('1', '2')
logger.info('1', '2', '3')
logger.debug('debug 1')
logger.debug('1', '2')
logger.debug('1', '2', '3')
logger = init(DEBUG)
logger.info('1')
logger.info('1', '2')
logger.info('1', '2', '3')
logger.debug('debug 2')
logger.debug('1', '2')
logger.debug('1', '2', '3')
================================================
FILE: PyPtt/screens.py
================================================
import re
import sys
from uao import register_uao
from . import log
register_uao()
class Target:
MainMenu = [
'離開,再見',
'人, 我是',
'[呼叫器]',
]
MainMenu_Exiting = [
'【主功能表】',
'您確定要離開',
]
PTT1_QueryPost = [
'請按任意鍵繼續',
'文章代碼(AID):',
'文章網址:'
]
PTT2_QueryPost = [
'請按任意鍵繼續',
'文章代碼(AID):'
]
InBoard = [
'看板資訊/設定',
'文章選讀',
'相關主題'
]
InBoardWithCursor = [
'【',
'看板資訊/設定',
]
InBoardWithCursorLen = len(InBoardWithCursor)
# (h)說明 (←/q)離開
# (y)回應(X%)推文(h)說明(←)離開
# (y)回應(X/%)推文 (←)離開
InPost = [
'瀏覽',
'頁',
')離開'
]
PostEnd = [
'瀏覽',
'頁 (100%)',
')離開'
]
InWaterBallList = [
'瀏覽',
'頁',
'說明',
]
WaterBallListEnd = [
'瀏覽',
'頁 (100%)',
'說明'
]
PostIP_New = [
'※ 發信站: 批踢踢實業坊(ptt.cc), 來自:'
]
PostIP_Old = [
'◆ From:'
]
Edit = [
'※ 編輯'
]
PostURL = [
'※ 文章網址'
]
Vote_Type1 = [
'◆ 投票名稱',
'◆ 投票中止於',
'◆ 票選題目描述'
]
Vote_Type2 = [
'投票名稱',
'◆ 預知投票紀事',
]
AnyKey = '任意鍵'
InTalk = [
'【聊天說話】',
'線上使用者列表',
'查詢網友',
'顯示上幾次熱訊'
]
InUserList = [
'休閒聊天',
'聊天/寫信',
'說明',
]
InMailBox = [
'【郵件選單】',
'[~]資源回收筒',
'鴻雁往返'
]
InMailBoxWithCursor = [
'【郵件選單】',
'[~]資源回收筒',
]
InMailBoxWithCursorLen = len(InMailBoxWithCursor)
InMailMenu = [
'【電子郵件】',
'我的信箱',
'把所有私人資料打包回去',
'寄信給帳號站長',
]
PostNoContent = [
'◆ 此文章無內容',
AnyKey
]
InBoardList = [
'【看板列表】',
'選擇看板',
'只列最愛',
'已讀/未讀'
]
use_too_many_resources = [
'程式耗用過多計算資源'
]
Animation = [
'★ 這份文件是可播放的文字動畫,要開始播放嗎?'
]
CursorToGoodbye = MainMenu.copy()
content_start = '─── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ──'
content_end_list = [
'--\n※ 發信站: 批踢踢實業坊',
'--\n※ 發信站: 批踢踢兔(ptt2.cc)',
'--\n※ 發信站: 新批踢踢(ptt2.twbbs.org.tw)'
]
def show(config, screen_queue, function_name=None):
if config.log_level != log.DEBUG:
return
if isinstance(screen_queue, list):
for Screen in screen_queue:
print('-' * 50)
try:
print(
Screen.encode(
sys.stdin.encoding, "replace").decode(
sys.stdin.encoding))
except Exception:
print(Screen.encode('utf-8', "replace").decode('utf-8'))
else:
print('-' * 50)
try:
print(screen_queue.encode(
sys.stdin.encoding, "replace").decode(
sys.stdin.encoding))
except Exception:
print(screen_queue.encode('utf-8', "replace").decode('utf-8'))
print('len:' + str(len(screen_queue)))
if function_name is not None:
print('錯誤在 ' + function_name + ' 函式發生')
print('-' * 50)
xy_pattern_h = re.compile('^=ESC=\[[\d]+;[\d]+H')
xy_pattern_s = re.compile('^=ESC=\[[\d]+;[\d]+s')
class VT100Parser:
def _h(self):
self._cursor_x = 0
self._cursor_y = 0
def _2j(self):
self.screen = [''] * 24
self.screen_length = dict()
def _move(self, x, y):
self._cursor_x = x
self._cursor_y = y
def _newline(self):
self._cursor_x = 0
self._cursor_y += 1
def _k(self):
if self._cursor_x == 0:
# nothing happen but cause error
return
self.screen[self._cursor_y] = self.screen[self._cursor_y][:self._cursor_x]
def __init__(self, bytes_data, encoding):
# self._data = data
# https://www.csie.ntu.edu.tw/~r88009/Java/html/Network/vt100.htm
self._cursor_x = 0
self._cursor_y = 0
self.screen = [''] * 24
self.screen_length = dict()
data = bytes_data.decode(encoding, errors='replace')
# remove color
data = re.sub('\x1B\[[\d+;]*m', '', data)
data = re.sub(r'[\x1B]', '=ESC=', data)
data = re.sub(r'[\r]', '', data)
while ' \x08' in data:
data = re.sub(r' \x08', '', data)
# print('---' * 8)
# print(encoding)
# print(bytes_data)
# print(data)
# print('---' * 8)
if '=ESC=[2J' in data:
data = data[data.rfind('=ESC=[2J') + len('=ESC=[2J'):]
count = 0
while data:
count += 1
while True:
if not data.startswith('=ESC='):
break
if data.startswith('=ESC=[H'):
data = data[len('=ESC=[H'):]
self._h()
continue
elif data.startswith('=ESC=[K'):
data = data[len('=ESC=[K'):]
self._k()
continue
elif data.startswith('=ESC=[s'):
data = data[len('=ESC=[s'):]
continue
break
xy_result = None
xy_result_h = xy_pattern_h.search(data)
if not xy_result_h:
xy_result_s = xy_pattern_s.search(data)
if xy_result_s:
xy_result = xy_result_s
else:
xy_result = xy_result_h
if xy_result:
xy_part = xy_result.group(0)
new_y = int(xy_part[6:xy_part.find(';')]) - 1
new_x = int(xy_part[xy_part.find(';') + 1: -1])
# log.py.info('xy', xy_part, new_x, new_y)
self._move(new_x, new_y)
data = data[len(xy_part):]
else:
if data.startswith('\n'):
data = data[1:]
self._newline()
continue
# print(f'-{data[:1]}-{len(data[:1].encode("big5-uao", "replace"))}')
if self._cursor_y not in self.screen_length:
self.screen_length[self._cursor_y] = len(self.screen[self._cursor_y].encode(encoding, 'replace'))
current_line_length = self.screen_length[self._cursor_y]
replace_mode = False
if current_line_length < self._cursor_x:
append_space = ' ' * (self._cursor_x - current_line_length)
self.screen[self._cursor_y] += append_space
elif current_line_length > self._cursor_x:
replace_mode = True
next_newline = data.find('\n')
next_newline = 1920 if next_newline < 0 else next_newline
next_esc = data.find('=ESC=')
next_esc = 1920 if next_esc < 0 else next_esc
if next_esc == 0:
break
current_index = min(next_newline, next_esc)
current_data = data[:current_index]
current_data_length = len(current_data.encode(encoding, 'replace'))
# print('=', current_data, '=', current_data_length)
if replace_mode:
current_line = self.screen[self._cursor_y][:self._cursor_x]
current_line += current_data
current_line += self.screen[self._cursor_y][self._cursor_x + len(current_data):]
self.screen[self._cursor_y] = current_line
else:
self.screen[self._cursor_y] += current_data
self._cursor_x += current_data_length
self.screen_length[self._cursor_y] = self._cursor_x
data = data[current_index:]
# print('\n'.join(self.screen))
# print('\n'.join(self._screen))
# print('=' * 20)
# print(data)
# print('Spend', count, 'cycle')
self.screen = '\n'.join(self.screen)
if __name__ == '__main__':
# post list
screen = b"\x1b[H\x1b[2J\x1b[1;37;44m\xe3\x80\x90\xe4\xb8\xbb\xe5\x8a\x9f\xe8\x83\xbd\xe8\xa1\xa8\xe3\x80\x91 \x1b[33m\xe6\x89\xb9\xe8\xb8\xa2\xe8\xb8\xa2\xe5\xaf\xa6\xe6\xa5\xad\xe5\x9d\x8a\x1b[0;1;37;44m \r\n \x08\x08\x1b[47m\xe2\x96\x88\x1b[2;3H \x08\x08\xe2\x96\x88\x1b[2;5H \x08\x08\xe2\x96\x88\x1b[2;7H \x08\x08\xe2\x96\x88\x1b[2;9H \x08\x08\xe2\x96\x88\x1b[2;11H \x08\x08\xe2\x96\x88\x1b[2;13H \x08\x08\xe2\x96\x88\x1b[2;15H \x08\x08\xe2\x96\x88\x1b[2;17H \x08\x08\xe2\x96\x88\x1b[2;19H \x08\x08\xe2\x96\x88\x1b[2;21H \x08\x08\xe2\x96\x88\x1b[2;23H \x08\x08\xe2\x96\x88\x1b[2;25H \x08\x08\xe2\x96\x88\x1b[2;27H \x08\x08\xe2\x96\x88\x1b[2;29H \x08\x08\xe2\x96\x88\x1b[2;31H \x08\x08\xe2\x96\x88\x1b[2;33H \x08\x08\xe2\x96\x88\x1b[2;35H \x08\x08\xe2\x96\x88\x1b[2;37H \x08\x08\xe2\x96\x88\x1b[2;39H \x08\x08\xe2\x96\x88\x1b[2;41H \x08\x08\xe2\x96\x88\x1b[2;43H \x08\x08\xe2\x96\x88\x1b[2;45H \x08\x08\xe2\x96\x88\x1b[2;47H \x08\x08\xe2\x96\x88\x1b[2;49H \x08\x08\xe2\x96\x88\x1b[2;51H \x08\x08\xe2\x96\x88\x1b[2;53H \x08\x08\xe2\x96\x88\x1b[2;55H \x08\x08\xe2\x96\x88\x1b[2;57H \x08\x08\xe2\x96\x88\x1b[2;59H \x08\x08\xe2\x96\x88\x1b[2;61H \x08\x08\xe2\x96\x88\x1b[2;63H \x08\x08\xe2\x96\x88\x1b[2;65H \x08\x08\xe2\x96\x88\x1b[2;67H \x08\x08\xe2\x96\x88\x1b[2;69H \x08\x08\xe2\x96\x88\x1b[2;71H \x08\x08\xe2\x96\x88\x1b[2;73H \x08\x08\xe2\x96\x88\x1b[2;75H \x08\x08\xe2\x96\x88\x1b[2;77H \x08\x08\xe2\x96\x88\x1b[2;79H\r\n \x08\x08\x1b[0;33;47m\xe2\x96\x85\x1b[3;3H \x08\x08\xe2\x96\x86\x1b[3;5H \x08\x08\xe2\x96\x86\x1b[3;7H \x08\x08\x1b[1;36;43m\xe2\x97\xa5\x1b[3;9H \x08\x08\x1b[47m\xe2\x96\x88\x1b[3;11H \x08\x08\xe2\x96\x88\x1b[3;13H \x08\x08\xe2\x96\x88\x1b[3;15H \x08\x08\xe2\x96\x88\x1b[3;17H \x08\x08\xe2\x96\x88\x1b[3;19H \x08\x08\xe2\x96\x88\x1b[3;21H \x08\x08\xe2\x96\x88\x1b[3;23H \x08\x08\xe2\x96\x88\x1b[3;25H \x08\x08\xe2\x96\x88\x1b[3;27H \x08\x08\xe2\x96\x88\x1b[3;29H \x08\x08\xe2\x96\x88\x1b[3;31H \x08\x08\xe2\x96\x88\x1b[3;33H \x08\x08\xe2\x96\x88\x1b[3;35H \x08\x08\xe2\x96\x88\x1b[3;37H \x08\x08\xe2\x96\x88\x1b[3;39H \x08\x08\xe2\x96\x88\x1b[3;41H \x08\x08\xe2\x96\x88\x1b[3;43H \x08\x08\xe2\x96\x88\x1b[3;45H \x08\x08\xe2\x96\x88\x1b[3;47H \x08\x08\xe2\x96\x88\x1b[3;49H \x08\x08\xe2\x96\x88\x1b[3;51H \x08\x08\xe2\x96\x88\x1b[3;53H \x08\x08\xe2\x96\x88\x1b[3;55H \x08\x08\xe2\x96\x88\x1b[3;57H \x08\x08\xe2\x96\x88\x1b[3;59H \x08\x08\xe2\x96\x88\x1b[3;61H \x08\x08\xe2\x96\x88\x1b[3;63H \x08\x08\xe2\x96\x88\x1b[3;65H \x08\x08\xe2\x96\x88\x1b[3;67H \x08\x08\xe2\x96\x88\x1b[3;69H \x08\x08\xe2\x96\x88\x1b[3;71H \x08\x08\xe2\x96\x88\x1b[3;73H \x08\x08\xe2\x96\x88\x1b[3;75H \x08\x08\xe2\x96\x88\x1b[3;77H \x08\x08\xe2\x96\x88\x1b[3;79H\r\n\x1b[0;30;43m - \x08\x08\xe2\x94\x80\x1b[4;9H` \x1b[1;36m\xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3\x1b[30m\xef\xbf\xa3\xef\xbf\xa3\x1b[36m\xef\xbf\xa3 \r\n\x1b[0;43m \x08\x08\x1b[31m\xe2\x96\x82\x1b[5;15H \x08\x08\xe2\x96\x83\x1b[5;17H\x1b[1;30m \xef\xb8\xb1 \xef\xbd\x9c \xef\xb9\xa8 \x1b[0;30;43m\xef\xbd\x9c \\ \x08\x08\xe2\x88\xa3\x1b[5;77H \r\n \\_ \x08\x08\xe2\x88\x95\x1b[6;9H \x08\x08\x1b[33;41m\xe2\x97\xa4\x1b[6;12H \x08\x08\x1b[0;41m\xe2\x96\x82\x1b[6;14H \x08\x08\x1b[31;43m\xe2\x96\x8a\x1b[6;17H\x1b[34m\xe3\x80\x82\x1b[30m / \x08\x08\xe2\x88\x95\x1b[6;27H \xef\xb9\x8d \xef\xb9\xa8 \x1b[1mL_ \x1b[0;30;43m| | \x08\x08\xe2\x8a\xa5\x1b[6;61H \x1b[1m\xef\xbd\x9c \x1b[0;30;43m. \x08\x08\xe2\x88\xa0\x1b[6;76H \r\n \x08\x08\xe2\x95\xb3\x1b[7;6H \x08\x08\x1b[31;47m\xe2\x96\x8a\x1b[7;12H \x08\x08\x1b[30m\xcb\x99\x1b[7;14H \x08\x08\x1b[0;41m\xe2\x96\x8e\x1b[7;16H \x08\x08\x1b[33m\xe2\x97\xa4\x1b[7;18H \x08\x08\x1b[31;43m\xe2\x96\x8e\x1b[7;20H\x1b[30m` \x08\x08\xe2\x94\x90\x1b[7;23H \x08\x08\xe2\x94\x82\x1b[7;26H, \xef\xb8\xb3 \x08\x08\xe2\x96\x81\x1b[7;34H \x08\x08\xe2\x95\x93\x1b[7;39H-.\xef\xb8\xbf \x08\x08\xe2\x88\x95\x1b[7;48H ` _\xef\xb8\xb3 \x08\x08\xe2\x88\xa3\x1b[7;58H . _x_ \\ \xef\xbd\x9c \x08\x08\xe2\x95\xb2\x1b[7;78H \r\n L \x1b[36m_ \x08\x08\xe2\x96\x84\x1b[8;8H \x08\x08\x1b[41m\xe2\x96\x8a\x1b[8;10H\x1b[30m\\ \x08\x08\x1b[31;47m\xe2\x96\x86\x1b[8;14H\x1b[1;36;41m' \x1b[0;30;43m N \xef\xbc\xbc_7_\xef\xb8\xb7-+ \x08\x08\xe2\x94\xa4\x1b[8;33H \x08\x08\xe2\x86\x96\x1b[8;36H \x08\x08\xe2\x95\xb3\x1b[8;38H \xe3\x80\x89 \x08\x08\xe2\x94\xac\x1b[8;45H' \x08\x08\xe2\x94\x8c\x1b[8;48H \x08\x08\xe2\x80\xb5\x1b[8;50H \x08\x08\xe2\x88\x9a\x1b[8;52H 7\xe2\x95\xb4.-\xef\xbc\x81 \x08\x08\xe2\x80\xb2\x1b[8;62H ` \x08\x08\xe2\x94\xac\x1b[8;68H+ \x08\x08\xe2\x94\xbc\x1b[8;71H=. \x08\x08\xe2\x88\x95\x1b[8;75H= \xe2\x95\xb4\r\n \x08\x08\xe2\x94\xa4\x1b[9;3H \x08\x08\x1b[33;46m\xe2\x96\x84\x1b[9;6H \x08\x08\x1b[31m\xe2\x96\x84\x1b[9;9H\x1b[30;41m, \x08\x08\xe2\x95\xb2\x1b[9;13H,_ \x08\x08\x1b[33m\xe2\x97\xa2\x1b[9;19H \x08\x08\x1b[30;43m\xe2\x94\x82\x1b[9;21H\\_ \x08\x08\xe2\x95\xb1\x1b[9;25H \x08\x08\xe2\x96\x8f\x1b[9;27H\\_ \x08\x08\xe2\x88\x95\x1b[9;31H_\xef\xb9\x80 _;} \x08\x08\xe2\x80\x94\x1b[9;41HL \x08\x08\xe2\x86\x98\x1b[9;46H_\xe3\x80\x95-_ \x08\x08\xe2\x95\xb3\x1b[9;53H | \xef\xbc\xbc__F \x08\x08\xe2\x86\x99\x1b[9;67H \xef\xb9\x8d] \x08\x08\xe2\x96\x8f\x1b[9;75H_ \x08\x08\xe2\x88\x95\x1b[9;78H \r\n \x08\x08\xe2\x95\xb2\x1b[10;4H\x1b[32mr \x08\x08\x1b[46m\xe2\x96\x8e\x1b[10;7H \x08\x08\x1b[31m\xe2\x97\xa5\x1b[10;9H\x1b[30;41m\xe3\x80\x83 \x08\x08\xe2\x94\x94\x1b[10;13H\x1b[34m_ \x08\x08\x1b[36m\xe2\x96\x84\x1b[10;16H\x1b[31;43m\xe3\x80\x9e \x08\x08\x1b[30m\xe2\x86\x97\x1b[10;20H \x08\x08\xe2\x95\xb3\x1b[10;22H \x08\x08\xe2\x96\x95\x1b[10;25H_ \x08\x08\xe2\x94\x98\x1b[10;29H \x08\x08\xe2\x95\xb3\x1b[10;31H \x08\x08\xe2\x95\xb2\x1b[10;34H_ \x08\x08\xe2\x86\x99\x1b[10;37H \x08\x08\xe2\x96\x8e\x1b[10;39H \x08\x08\xe2\x86\x99\x1b[10;41H \x08\x08\xe2\x86\x91\x1b[10;43H_ \x08\x08\xe2\x96\x95\x1b[10;46H , \x08\x08\xe2\x94\xbc\x1b[10;51H \x08\x08\xe2\x96\x95\x1b[10;53H \xef\xb9\xa8 \xef\xb9\x80` \x08\x08\xe2\x96\x8e\x1b[10;66H`\xef\xbf\xa3 \x08\x08\xe2\x86\x96\x1b[10;73H\xef\xbf\xa3} \x08\x08\xe2\x95\xb2\x1b[10;79H\r\n r \x08\x08\xe2\x94\x98\x1b[11;5H \x08\x08\x1b[33;46m\xe2\x96\x84\x1b[11;8H \x08\x08\x1b[31m\xe2\x97\xa5\x1b[11;11H \x08\x08\x1b[33m\xe2\x96\x83\x1b[11;13H \x08\x08\xe2\x96\x86\x1b[11;15H \x08\x08\x1b[30;43m\xe2\x94\xac\x1b[11;17H \x08\x08\xe2\x95\x9d\x1b[11;19H \x08\x08\xe2\x94\x94\x1b[11;21H \x08\x08\xe2\x96\x8e\x1b[11;24H \x08\x08\xe2\x95\xb2\x1b[11;28H ` \x08\x08\xe2\x96\x8f\x1b[11;33H \x08\x08\xe2\x94\x9c\x1b[11;35H \xef\xbf\xa3\\ \x08\x08\xe2\x80\x99\x1b[11;44H\xef\xb8\xba \x08\x08\xe2\x96\x8f\x1b[11;48H\x1b[35m\xe5\x8f\xaf\xe4\xb8\x8d\xe5\x8f\xaf\xe4\xbb\xa5\xe5\x81\xb6\xe7\x88\xbe\xe4\xb8\x8b\xe9\x9b\xa8\xe4\xb8\x8d\xe5\xbf\x85\xe6\xb0\xb8\xe9\x81\xa0\xe6\x99\xb4\xe5\xa4\xa9...\x1b[13;23H\x1b[m(\x1b[1;36mA\x1b[m)nnounce \xe3\x80\x90 \xe7\xb2\xbe\xe8\x8f\xaf\xe5\x85\xac\xe4\xbd\x88\xe6\xac\x84 \xe3\x80\x91\x1b[14;23H(\x1b[1;36mF\x1b[m)avorite \xe3\x80\x90 \xe6\x88\x91 \xe7\x9a\x84 \xe6\x9c\x80\xe6\x84\x9b \xe3\x80\x91\x1b[15;21H> (\x1b[1;36mC\x1b[m)lass\x1b[15;38H\xe3\x80\x90 \xe5\x88\x86\xe7\xb5\x84\xe8\xa8\x8e\xe8\xab\x96\xe5\x8d\x80 \xe3\x80\x91\x1b[16;23H(\x1b[1;36mM\x1b[m)ail\x1b[16;38H\xe3\x80\x90 \xe7\xa7\x81\xe4\xba\xba\xe4\xbf\xa1\xe4\xbb\xb6\xe5\x8d\x80 \xe3\x80\x91\x1b[17;23H(\x1b[1;36mT\x1b[m)alk\x1b[17;38H\xe3\x80\x90 \xe4\xbc\x91\xe9\x96\x92\xe8\x81\x8a\xe5\xa4\xa9\xe5\x8d\x80 \xe3\x80\x91\x1b[18;23H(\x1b[1;36mU\x1b[m)ser\x1b[18;38H\xe3\x80\x90 \xe5\x80\x8b\xe4\xba\xba\xe8\xa8\xad\xe5\xae\x9a\xe5\x8d\x80 \xe3\x80\x91\x1b[19;23H(\x1b[1;36mX\x1b[m)yz\x1b[19;38H\xe3\x80\x90 \xe7\xb3\xbb\xe7\xb5\xb1\xe8\xb3\x87\xe8\xa8\x8a\xe5\x8d\x80 \xe3\x80\x91\x1b[20;23H(\x1b[1;36mP\x1b[m)lay\x1b[20;38H\xe3\x80\x90 \xe5\xa8\x9b\xe6\xa8\x82\xe8\x88\x87\xe4\xbc\x91\xe9\x96\x92 \xe3\x80\x91\x1b[21;23H(\x1b[1;36mN\x1b[m)amelist \xe3\x80\x90 \xe7\xb7\xa8\xe7\x89\xb9\xe5\x88\xa5\xe5\x90\x8d\xe5\x96\xae \xe3\x80\x91\x1b[22;23H(\x1b[1;36mG\x1b[m)oodbye\x1b[22;41H\xe9\x9b\xa2\xe9\x96\x8b\xef\xbc\x8c\xe5\x86\x8d\xe8\xa6\x8b \x08\x08\xe2\x80\xa6\x1b[22;53H\r\n\n\x1b[34;46m[12/4 \xe6\x98\x9f\xe6\x9c\x9f\xe5\x85\xad 14:26]\x1b[1;33;45m [ \xe9\x9b\x99\xe9\xad\x9a\xe6\x99\x82 ] \x1b[30;47m \xe7\xb7\x9a\xe4\xb8\x8a\x1b[31m66391\x1b[30m\xe4\xba\xba, \xe6\x88\x91\xe6\x98\xaf\x1b[31mCodingMan\x1b[30m [\xe5\x91\xbc\xe5\x8f\xab\xe5\x99\xa8]\x1b[31m\xe9\x97\x9c\xe9\x96\x89 \x1b[m\x1b[15;21H\x1b[H\x1b[2J\x1b[1;37;44m\xe3\x80\x90\xe4\xb8\xbb\xe5\x8a\x9f\xe8\x83\xbd\xe8\xa1\xa8\xe3\x80\x91 \x1b[33m\xe6\x89\xb9\xe8\xb8\xa2\xe8\xb8\xa2\xe5\xaf\xa6\xe6\xa5\xad\xe5\x9d\x8a\x1b[0;1;37;44m \r\n \x08\x08\x1b[47m\xe2\x96\x88\x1b[2;3H \x08\x08\xe2\x96\x88\x1b[2;5H \x08\x08\xe2\x96\x88\x1b[2;7H \x08\x08\xe2\x96\x88\x1b[2;9H \x08\x08\xe2\x96\x88\x1b[2;11H \x08\x08\xe2\x96\x88\x1b[2;13H \x08\x08\xe2\x96\x88\x1b[2;15H \x08\x08\xe2\x96\x88\x1b[2;17H \x08\x08\xe2\x96\x88\x1b[2;19H \x08\x08\xe2\x96\x88\x1b[2;21H \x08\x08\xe2\x96\x88\x1b[2;23H \x08\x08\xe2\x96\x88\x1b[2;25H \x08\x08\xe2\x96\x88\x1b[2;27H \x08\x08\xe2\x96\x88\x1b[2;29H \x08\x08\xe2\x96\x88\x1b[2;31H \x08\x08\xe2\x96\x88\x1b[2;33H \x08\x08\xe2\x96\x88\x1b[2;35H \x08\x08\xe2\x96\x88\x1b[2;37H \x08\x08\xe2\x96\x88\x1b[2;39H \x08\x08\xe2\x96\x88\x1b[2;41H \x08\x08\xe2\x96\x88\x1b[2;43H \x08\x08\xe2\x96\x88\x1b[2;45H \x08\x08\xe2\x96\x88\x1b[2;47H \x08\x08\xe2\x96\x88\x1b[2;49H \x08\x08\xe2\x96\x88\x1b[2;51H \x08\x08\xe2\x96\x88\x1b[2;53H \x08\x08\xe2\x96\x88\x1b[2;55H \x08\x08\xe2\x96\x88\x1b[2;57H \x08\x08\xe2\x96\x88\x1b[2;59H \x08\x08\xe2\x96\x88\x1b[2;61H \x08\x08\xe2\x96\x88\x1b[2;63H \x08\x08\xe2\x96\x88\x1b[2;65H \x08\x08\xe2\x96\x88\x1b[2;67H \x08\x08\xe2\x96\x88\x1b[2;69H \x08\x08\xe2\x96\x88\x1b[2;71H \x08\x08\xe2\x96\x88\x1b[2;73H \x08\x08\xe2\x96\x88\x1b[2;75H \x08\x08\xe2\x96\x88\x1b[2;77H \x08\x08\xe2\x96\x88\x1b[2;79H\r\n \x08\x08\x1b[0;33;47m\xe2\x96\x85\x1b[3;3H \x08\x08\xe2\x96\x86\x1b[3;5H \x08\x08\xe2\x96\x86\x1b[3;7H \x08\x08\x1b[1;36;43m\xe2\x97\xa5\x1b[3;9H \x08\x08\x1b[47m\xe2\x96\x88\x1b[3;11H \x08\x08\xe2\x96\x88\x1b[3;13H \x08\x08\xe2\x96\x88\x1b[3;15H \x08\x08\xe2\x96\x88\x1b[3;17H \x08\x08\xe2\x96\x88\x1b[3;19H \x08\x08\xe2\x96\x88\x1b[3;21H \x08\x08\xe2\x96\x88\x1b[3;23H \x08\x08\xe2\x96\x88\x1b[3;25H \x08\x08\xe2\x96\x88\x1b[3;27H \x08\x08\xe2\x96\x88\x1b[3;29H \x08\x08\xe2\x96\x88\x1b[3;31H \x08\x08\xe2\x96\x88\x1b[3;33H \x08\x08\xe2\x96\x88\x1b[3;35H \x08\x08\xe2\x96\x88\x1b[3;37H \x08\x08\xe2\x96\x88\x1b[3;39H \x08\x08\xe2\x96\x88\x1b[3;41H \x08\x08\xe2\x96\x88\x1b[3;43H \x08\x08\xe2\x96\x88\x1b[3;45H \x08\x08\xe2\x96\x88\x1b[3;47H \x08\x08\xe2\x96\x88\x1b[3;49H \x08\x08\xe2\x96\x88\x1b[3;51H \x08\x08\xe2\x96\x88\x1b[3;53H \x08\x08\xe2\x96\x88\x1b[3;55H \x08\x08\xe2\x96\x88\x1b[3;57H \x08\x08\xe2\x96\x88\x1b[3;59H \x08\x08\xe2\x96\x88\x1b[3;61H \x08\x08\xe2\x96\x88\x1b[3;63H \x08\x08\xe2\x96\x88\x1b[3;65H \x08\x08\xe2\x96\x88\x1b[3;67H \x08\x08\xe2\x96\x88\x1b[3;69H \x08\x08\xe2\x96\x88\x1b[3;71H \x08\x08\xe2\x96\x88\x1b[3;73H \x08\x08\xe2\x96\x88\x1b[3;75H \x08\x08\xe2\x96\x88\x1b[3;77H \x08\x08\xe2\x96\x88\x1b[3;79H\r\n\x1b[0;30;43m - \x08\x08\xe2\x94\x80\x1b[4;9H` \x1b[1;36m\xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3 \xef\xbf\xa3\xef\xbf\xa3\xef\xbf\xa3\x1b[30m\xef\xbf\xa3\xef\xbf\xa3\x1b[36m\xef\xbf\xa3 \r\n\x1b[0;43m \x08\x08\x1b[31m\xe2\x96\x82\x1b[5;15H \x08\x08\xe2\x96\x83\x1b[5;17H\x1b[1;30m \xef\xb8\xb1 \xef\xbd\x9c \xef\xb9\xa8 \x1b[0;30;43m\xef\xbd\x9c \\ \x08\x08\xe2\x88\xa3\x1b[5;77H \r\n \\_ \x08\x08\xe2\x88\x95\x1b[6;9H \x08\x08\x1b[33;41m\xe2\x97\xa4\x1b[6;12H \x08\x08\x1b[0;41m\xe2\x96\x82\x1b[6;14H \x08\x08\x1b[31;43m\xe2\x96\x8a\x1b[6;17H\x1b[34m\xe3\x80\x82\x1b[30m / \x08\x08\xe2\x88\x95\x1b[6;27H \xef\xb9\x8d \xef\xb9\xa8 \x1b[1mL_ \x1b[0;30;43m| | \x08\x08\xe2\x8a\xa5\x1b[6;61H \x1b[1m\xef\xbd\x9c \x1b[0;30;43m. \x08\x08\xe2\x88\xa0\x1b[6;76H \r\n \x08\x08\xe2\x95\xb3\x1b[7;6H \x08\x08\x1b[31;47m\xe2\x96\x8a\x1b[7;12H \x08\x08\x1b[30m\xcb\x99\x1b[7;14H \x08\x08\x1b[0;41m\xe2\x96\x8e\x1b[7;16H \x08\x08\x1b[33m\xe2\x97\xa4\x1b[7;18H \x08\x08\x1b[31;43m\xe2\x96\x8e\x1b[7;20H\x1b[30m` \x08\x08\xe2\x94\x90\x1b[7;23H \x08\x08\xe2\x94\x82\x1b[7;26H, \xef\xb8\xb3 \x08\x08\xe2\x96\x81\x1b[7;34H \x08\x08\xe2\x95\x93\x1b[7;39H-.\xef\xb8\xbf \x08\x08\xe2\x88\x95\x1b[7;48H ` _\xef\xb8\xb3 \x08\x08\xe2\x88\xa3\x1b[7;58H . _x_ \\ \xef\xbd\x9c \x08\x08\xe2\x95\xb2\x1b[7;78H \r\n L \x1b[36m_ \x08\x08\xe2\x96\x84\x1b[8;8H \x08\x08\x1b[41m\xe2\x96\x8a\x1b[8;10H\x1b[30m\\ \x08\x08\x1b[31;47m\xe2\x96\x86\x1b[8;14H\x1b[1;36;41m' \x1b[0;30;43m N \xef\xbc\xbc_7_\xef\xb8\xb7-+ \x08\x08\xe2\x94\xa4\x1b[8;33H \x08\x08\xe2\x86\x96\x1b[8;36H \x08\x08\xe2\x95\xb3\x1b[8;38H \xe3\x80\x89 \x08\x08\xe2\x94\xac\x1b[8;45H' \x08\x08\xe2\x94\x8c\x1b[8;48H \x08\x08\xe2\x80\xb5\x1b[8;50H \x08\x08\xe2\x88\x9a\x1b[8;52H 7\xe2\x95\xb4.-\xef\xbc\x81 \x08\x08\xe2\x80\xb2\x1b[8;62H ` \x08\x08\xe2\x94\xac\x1b[8;68H+ \x08\x08\xe2\x94\xbc\x1b[8;71H=. \x08\x08\xe2\x88\x95\x1b[8;75H= \xe2\x95\xb4\r\n \x08\x08\xe2\x94\xa4\x1b[9;3H \x08\x08\x1b[33;46m\xe2\x96\x84\x1b[9;6H \x08\x08\x1b[31m\xe2\x96\x84\x1b[9;9H\x1b[30;41m, \x08\x08\xe2\x95\xb2\x1b[9;13H,_ \x08\x08\x1b[33m\xe2\x97\xa2\x1b[9;19H \x08\x08\x1b[30;43m\xe2\x94\x82\x1b[9;21H\\_ \x08\x08\xe2\x95\xb1\x1b[9;25H \x08\x08\xe2\x96\x8f\x1b[9;27H\\_ \x08\x08\xe2\x88\x95\x1b[9;31H_\xef\xb9\x80 _;} \x08\x08\xe2\x80\x94\x1b[9;41HL \x08\x08\xe2\x86\x98\x1b[9;46H_\xe3\x80\x95-_ \x08\x08\xe2\x95\xb3\x1b[9;53H | \xef\xbc\xbc__F \x08\x08\xe2\x86\x99\x1b[9;67H \xef\xb9\x8d] \x08\x08\xe2\x96\x8f\x1b[9;75H_ \x08\x08\xe2\x88\x95\x1b[9;78H \r\n \x08\x08\xe2\x95\xb2\x1b[10;4H\x1b[32mr \x08\x08\x1b[46m\xe2\x96\x8e\x1b[10;7H \x08\x08\x1b[31m\xe2\x97\xa5\x1b[10;9H\x1b[30;41m\xe3\x80\x83 \x08\x08\xe2\x94\x94\x1b[10;13H\x1b[34m_ \x08\x08\x1b[36m\xe2\x96\x84\x1b[10;16H\x1b[31;43m\xe3\x80\x9e \x08\x08\x1b[30m\xe2\x86\x97\x1b[10;20H \x08\x08\xe2\x95\xb3\x1b[10;22H \x08\x08\xe2\x96\x95\x1b[10;25H_ \x08\x08\xe2\x94\x98\x1b[10;29H \x08\x08\xe2\x95\xb3\x1b[10;31H \x08\x08\xe2\x95\xb2\x1b[10;34H_ \x08\x08\xe2\x86\x99\x1b[10;37H \x08\x08\xe2\x96\x8e\x1b[10;39H \x08\x08\xe2\x86\x99\x1b[10;41H \x08\x08\xe2\x86\x91\x1b[10;43H_ \x08\x08\xe2\x96\x95\x1b[10;46H , \x08\x08\xe2\x94\xbc\x1b[10;51H \x08\x08\xe2\x96\x95\x1b[10;53H \xef\xb9\xa8 \xef\xb9\x80` \x08\x08\xe2\x96\x8e\x1b[10;66H`\xef\xbf\xa3 \x08\x08\xe2\x86\x96\x1b[10;73H\xef\xbf\xa3} \x08\x08\xe2\x95\xb2\x1b[10;79H\r\n r \x08\x08\xe2\x94\x98\x1b[11;5H \x08\x08\x1b[33;46m\xe2\x96\x84\x1b[11;8H \x08\x08\x1b[31m\xe2\x97\xa5\x1b[11;11H \x08\x08\x1b[33m\xe2\x96\x83\x1b[11;13H \x08\x08\xe2\x96\x86\x1b[11;15H \x08\x08\x1b[30;43m\xe2\x94\xac\x1b[11;17H \x08\x08\xe2\x95\x9d\x1b[11;19H \x08\x08\xe2\x94\x94\x1b[11;21H \x08\x08\xe2\x96\x8e\x1b[11;24H \x08\x08\xe2\x95\xb2\x1b[11;28H ` \x08\x08\xe2\x96\x8f\x1b[11;33H \x08\x08\xe2\x94\x9c\x1b[11;35H \xef\xbf\xa3\\ \x08\x08\xe2\x80\x99\x1b[11;44H\xef\xb8\xba \x08\x08\xe2\x96\x8f\x1b[11;48H\x1b[35m\xe5\x8f\xaf\xe4\xb8\x8d\xe5\x8f\xaf\xe4\xbb\xa5\xe5\x81\xb6\xe7\x88\xbe\xe4\xb8\x8b\xe9\x9b\xa8\xe4\xb8\x8d\xe5\xbf\x85\xe6\xb0\xb8\xe9\x81\xa0\xe6\x99\xb4\xe5\xa4\xa9...\x1b[13;23H\x1b[m(\x1b[1;36mA\x1b[m)nnounce \xe3\x80\x90 \xe7\xb2\xbe\xe8\x8f\xaf\xe5\x85\xac\xe4\xbd\x88\xe6\xac\x84 \xe3\x80\x91\x1b[14;23H(\x1b[1;36mF\x1b[m)avorite \xe3\x80\x90 \xe6\x88\x91 \xe7\x9a\x84 \xe6\x9c\x80\xe6\x84\x9b \xe3\x80\x91\x1b[15;21H> (\x1b[1;36mC\x1b[m)lass\x1b[15;38H\xe3\x80\x90 \xe5\x88\x86\xe7\xb5\x84\xe8\xa8\x8e\xe8\xab\x96\xe5\x8d\x80 \xe3\x80\x91\x1b[16;23H(\x1b[1;36mM\x1b[m)ail\x1b[16;38H\xe3\x80\x90 \xe7\xa7\x81\xe4\xba\xba\xe4\xbf\xa1\xe4\xbb\xb6\xe5\x8d\x80 \xe3\x80\x91\x1b[17;23H(\x1b[1;36mT\x1b[m)alk\x1b[17;38H\xe3\x80\x90 \xe4\xbc\x91\xe9\x96\x92\xe8\x81\x8a\xe5\xa4\xa9\xe5\x8d\x80 \xe3\x80\x91\x1b[18;23H(\x1b[1;36mU\x1b[m)ser\x1b[18;38H\xe3\x80\x90 \xe5\x80\x8b\xe4\xba\xba\xe8\xa8\xad\xe5\xae\x9a\xe5\x8d\x80 \xe3\x80\x91\x1b[19;23H(\x1b[1;36mX\x1b[m)yz\x1b[19;38H\xe3\x80\x90 \xe7\xb3\xbb\xe7\xb5\xb1\xe8\xb3\x87\xe8\xa8\x8a\xe5\x8d\x80 \xe3\x80\x91\x1b[20;23H(\x1b[1;36mP\x1b[m)lay\x1b[20;38H\xe3\x80\x90 \xe5\xa8\x9b\xe6\xa8\x82\xe8\x88\x87\xe4\xbc\x91\xe9\x96\x92 \xe3\x80\x91\x1b[21;23H(\x1b[1;36mN\x1b[m)amelist \xe3\x80\x90 \xe7\xb7\xa8\xe7\x89\xb9\xe5\x88\xa5\xe5\x90\x8d\xe5\x96\xae \xe3\x80\x91\x1b[22;23H(\x1b[1;36mG\x1b[m)oodbye\x1b[22;41H\xe9\x9b\xa2\xe9\x96\x8b\xef\xbc\x8c\xe5\x86\x8d\xe8\xa6\x8b \x08\x08\xe2\x80\xa6\x1b[22;53H\r\n\n\x1b[34;46m[12/4 \xe6\x98\x9f\xe6\x9c\x9f\xe5\x85\xad 14:26]\x1b[1;33;45m [ \xe9\x9b\x99\xe9\xad\x9a\xe6\x99\x82 ] \x1b[30;47m \xe7\xb7\x9a\xe4\xb8\x8a\x1b[31m66391\x1b[30m\xe4\xba\xba, \xe6\x88\x91\xe6\x98\xaf\x1b[31mCodingMan\x1b[30m [\xe5\x91\xbc\xe5\x8f\xab\xe5\x99\xa8]\x1b[31m\xe9\x97\x9c\xe9\x96\x89 \x1b[m\x1b[15;21H\x1b[H\x1b[2J\x1b[1;37;44m\xe3\x80\x90\xe6\x9d\xbf\xe4\xb8\xbb:catcatcatcat\xe3\x80\x91 \x1b[33mPython \xe7\xa8\x8b\xe5\xbc\x8f\xe8\xaa\x9e\xe8\xa8\x80\x1b[0;1;37;44m \xe7\x9c\x8b\xe6\x9d\xbf\xe3\x80\x8aPython\xe3\x80\x8b\r\n\x1b[m[ \x08\x08\xe2\x86\x90\x1b[2;4H]\xe9\x9b\xa2\xe9\x96\x8b [ \x08\x08\xe2\x86\x92\x1b[2;13H]\xe9\x96\xb1\xe8\xae\x80 [Ctrl-P]\xe7\x99\xbc\xe8\xa1\xa8\xe6\x96\x87\xe7\xab\xa0 [d]\xe5\x88\xaa\xe9\x99\xa4 [z]\xe7\xb2\xbe\xe8\x8f\xaf\xe5\x8d\x80 [i]\xe7\x9c\x8b\xe6\x9d\xbf\xe8\xb3\x87\xe8\xa8\x8a/\xe8\xa8\xad\xe5\xae\x9a [h]\xe8\xaa\xaa\xe6\x98\x8e\r\n\x1b[30;47m \xe7\xb7\xa8\xe8\x99\x9f \xe6\x97\xa5 \xe6\x9c\x9f \xe4\xbd\x9c \xe8\x80\x85 \xe6\x96\x87 \xe7\xab\xa0 \xe6\xa8\x99 \xe9\xa1\x8c \xe4\xba\xba\xe6\xb0\xa3:5 \x1b[4;4H\x1b[m8861 +\x1b[1;32m 2\x1b[m11/18 zj4gjcl6 \x08\x08\xe2\x96\xa1\x1b[4;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe7\xb0\xa1\xe5\x96\xae\xe7\x9a\x84\xe6\xad\xa3\xe5\x89\x87\xe8\xa1\xa8\xe9\x81\x94\xe5\xbc\x8f\xe8\xa8\x98\xe6\xb3\x95?\x1b[5;4H8862 +\x1b[1;32m 1\x1b[m11/18 d8888\x1b[5;31HR: [\xe9\x96\x92\xe8\x81\x8a] \xe6\x95\xb8\xe5\xad\xb8\xe4\xb8\x8d\xe5\xa5\xbd\xe6\x80\x8e\xe9\xba\xbc\xe7\x8e\xa9AI??\x1b[6;4H8863 +\x1b[1;32m 7\x1b[m11/18 \x1b[1;37mVivianAnn \x08\x08\x1b[m\xe2\x96\xa1\x1b[6;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe7\x94\xa8pip\xe7\x84\xa1\xe6\xb3\x95\xe5\xae\x89\xe8\xa3\x9dlibyang\x1b[7;4H8864 +\x1b[1;30m \x1b[m11/18 garlic774 \x08\x08\xe2\x96\xa1\x1b[7;33H [\xe9\x96\x92\xe8\x81\x8a] \xe8\xab\x8b\xe6\x95\x99\xe5\xa6\x82\xe4\xbd\x95\xe6\x8a\x93class\xe4\xb8\x8b\xe9\x9d\xa2\xe7\x9a\x84\xe8\xb3\x87\xe8\xa8\x8a\x1b[8;4H8865 +\x1b[1;30m \x1b[m11/20 garlic774 \x08\x08\xe2\x96\xa1\x1b[8;33H [\xe5\x95\x8f\xe9\xa1\x8c] Xpath\xe6\x8a\x93\xe4\xb8\x8d\xe5\x88\xb0\xe5\x85\xa7\xe5\xae\xb9\x1b[9;4H8866 +\x1b[1;32m 1\x1b[m11/22 g919233 \x08\x08\xe2\x96\xa1\x1b[9;33H [\xe6\x95\x99\xe5\xad\xb8] \xe7\x80\x8f\xe8\xa6\xbd\xe5\x99\xa8\xe8\x87\xaa\xe5\x8b\x95\xe5\x8c\x96\xe5\xb7\xa5\xe5\x85\xb7 Playwright\xef\xbc\x8c\xe7\x94\xa8\xe6\x96\xbc\xe7\xb6\xb2\x1b[10;4H8867 ~\x1b[1;32m 3\x1b[m11/23 ajjj840569 \x08\x08\xe2\x96\xa1\x1b[10;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe5\xa6\x82\xe6\x9e\x9c\xe8\xae\x93\xe7\x84\xa1\xe9\x96\x93\xe9\x9a\x94\xe7\x9a\x84\xe9\x80\xa3\xe7\xba\x8c\xe5\xad\x97 \xe6\x8e\xa8\xe5\xbe\x97\xe5\xad\x97\xe5\x85\xb8\xe5\xb0\x8d\xe6\x87\x89\xe5\x80\xbc\xef\xbc\x9f\x1b[11;4H8868 +\x1b[1;32m 2\x1b[m11/23 giuk0717 \x08\x08\xe2\x96\xa1\x1b[11;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe6\x89\xbe\xe5\x87\xba\xe6\x9c\x80\xe9\x95\xb7\xe9\x9b\x86\xe5\x90\x88\x1b[12;4H8869 +\x1b[1;30m \x1b[m11/23 areyo\x1b[12;31H \x08\x08\xe2\x96\xa1\x1b[12;33H [\xe5\x95\x8f\xe9\xa1\x8c] django + apache print\xe4\xb8\xad\xe6\x96\x87\x1b[13;4H8870 +\x1b[1;32m 3\x1b[m11/24 dreambegins \x08\x08\xe2\x96\xa1\x1b[13;33H [\xe5\x95\x8f\xe9\xa1\x8c] Robot Framework\xe7\x9a\x84\xe8\xaa\x9e\xe6\xb3\x95\xe5\x95\x8f\xe9\xa1\x8c\x1b[14;4H8871 +\x1b[1;30m \x1b[m11/27 DiamondAse \x08\x08\xe2\x96\xa1\x1b[14;33H [\xe5\x95\x8f\xe9\xa1\x8c] selenium\xe6\x8a\x93chrom\xe9\x96\x8b\xe8\xb5\xb7\xe7\x9a\x84pdf\xe7\xb6\xb2\xe9\xa0\x81\xe5\x85\x83\xe7\xb4\xa0\xe6\x8a\x93\xe4\xb8\x8d\xe5\x88\xb0\x1b[15;4H8872 +\x1b[1;32m 1\x1b[m11/28 garlic774 \x08\x08\xe2\x96\xa1\x1b[15;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe5\xa6\x82\xe4\xbd\x95\xe5\x9c\xa8chrome\xe6\x93\xb4\xe5\x85\x85\xe5\xbe\x97\xe5\x88\xb0\xe6\xaa\xa2\xe8\xa6\x96\xe5\x85\x83\xe7\xb4\xa0\xef\xbc\x9f\x1b[16;4H8873 +\x1b[1;30m \x1b[m11/28 ruthertw \x08\x08\xe2\x96\xa1\x1b[16;33H [\xe5\x95\x8f\xe9\xa1\x8c] np.transpose\xe7\x9a\x84\xe7\x94\xa8\xe6\xb3\x95\x1b[17;4H8874 +\x1b[1;30m \x1b[m11/28 nicha115 \x08\x08\xe2\x96\xa1\x1b[17;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe8\xab\x8b\xe5\x95\x8ftrace\xe5\x8e\x9f\xe5\xa7\x8b\xe7\xa2\xbc\xe5\x95\x8f\xe9\xa1\x8c\x1b[18;4H8875 +\x1b[1;33m10\x1b[m11/29 \x1b[1;37mVivianAnn \x08\x08\x1b[m\xe2\x96\xa1\x1b[18;33H [\xe5\x95\x8f\xe9\xa1\x8c] leetcode 2029 (Hard) \xe7\x9a\x84\xe5\x95\x8f\xe9\xa1\x8c\x1b[19;4H8876 +\x1b[1;32m 3\x1b[m11/29 a199111222 \x08\x08\xe2\x96\xa1\x1b[19;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe7\x88\xac\xe8\x9f\xb2\xe7\xaa\x81\xe7\x84\xb6\xe4\xb8\x8d\xe8\x83\xbd\xe8\xb7\x91\xef\xbc\x8c\xe6\xb1\x82\xe8\xa7\xa3\x1b[20;4H8877 +\x1b[1;32m 1\x1b[m11/30 Rasin\x1b[20;31H \x08\x08\xe2\x96\xa1\x1b[20;33H [\xe5\x95\x8f\xe9\xa1\x8c] numpy dimension\x1b[21;4H8878 +\x1b[1;32m 3\x1b[m11/30 Moonmoon0827 \x08\x08\xe2\x96\xa1\x1b[21;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe6\x96\xb0\xe6\x89\x8b list to string \xe7\x9a\x84\xe5\x95\x8f\xe9\xa1\x8c\x1b[22;4H8879 +\x1b[1;32m 3\x1b[m12/01 m0911182606 \x08\x08\xe2\x96\xa1\x1b[22;33H [\xe5\x95\x8f\xe9\xa1\x8c] \xe8\xae\x80\xe5\x8f\x96/\xe4\xbf\xae\xe6\x94\xb9\xe6\xaa\x94\xe6\xa1\x88\xe5\x85\xa7\xe5\xae\xb9\xe6\x8c\x87\xe5\xae\x9a\xe5\x8d\x80\xe9\x96\x93\xe6\x96\x87\xe5\xad\x97\r\n> 8880 ~\x1b[1;32m 4\x1b[m12/02 stepfish \x08\x08\xe2\x96\xa1\x1b[23;33H [\xe8\xb3\x87\xe8\xa8\x8a] \xe5\xb0\x88\xe5\xb1\xac\xe5\xa5\xb3\xe7\x94\x9f\xe7\x9a\x84Python\xe5\x85\xa5\xe9\x96\x80\xe8\xaa\xb2\xef\xbc\x88Pyladies\xe4\xb8\xbb\xe8\xbe\xa6\xef\xbc\x89\r\n\x1b[34;46m \xe6\x96\x87\xe7\xab\xa0\xe9\x81\xb8\xe8\xae\x80 \x1b[30;47m \x1b[31m(y)\x1b[30m\xe5\x9b\x9e\xe6\x87\x89\x1b[31m(X)\x1b[30m\xe6\x8e\xa8\xe6\x96\x87\x1b[31m(^X)\x1b[30m\xe8\xbd\x89\xe9\x8c\x84 \x1b[31m(=[]<>)\x1b[30m\xe7\x9b\xb8\xe9\x97\x9c\xe4\xb8\xbb\xe9\xa1\x8c\x1b[31m(/?a)\x1b[30m\xe6\x89\xbe\xe6\xa8\x99\xe9\xa1\x8c/\xe4\xbd\x9c\xe8\x80\x85 \x1b[31m(b)\x1b[30m\xe9\x80\xb2\xe6\x9d\xbf\xe7\x95\xab\xe9\x9d\xa2 \x1b[m\x1b[23;1H"
# main menu
screen = b'7;61H \x08\x08\x1b[m\xe2\x97\xa4\x1b[7;65H \x08\x08\xe2\x80\x94\x1b[7;73H\xef\xbc\x8b\x1b[8;17H1 \xe5\xb8\x82\xe6\xb0\x91\xe5\xbb\xa3\xe5\xa0\xb4 \xe5\xa0\xb1\xe5\x91\x8a\xe7\xab\x99\xe9\x95\xb7 PTT\xe5\x92\xac\xe6\x88\x91\x1b[9;17H2 \xe8\x87\xba\xe7\x81\xa3\xe5\xa4\xa7\xe5\xad\xb8 \xe8\x87\xba\xe5\xa4\xa7, \xe8\x87\xba\xe5\xa4\xa7, \xe8\x87\xba\xe5\xa4\xa7\x1b[9;62H[paullai/s75\x1b[10;17H3 \xe6\x94\xbf\xe6\xb2\xbb\xe5\xa4\xa7\xe5\xad\xb8 \xe6\x94\xbf\xe5\xa4\xa7, \xe6\x94\xbf\xe5\xa4\xa7, \xe6\x94\xbf\xe5\xa4\xa7\x1b[10;62H[steve1121/s\x1b[11;17H4 \xe9\x9d\x92\xe8\x98\x8b\xe6\x9e\x9c\xe6\xa8\xb9 \xe6\xa0\xa1\xe5\x9c\x92, \xe7\x8f\xad\xe6\x9d\xbf, \xe7\xa4\xbe\xe5\x9c\x98\x1b[11;62H[dreamwave/f\x1b[12;17H5 \xe6\xb4\xbb\xe5\x8b\x95\xe4\xb8\xad\xe5\xbf\x83 \xe7\xa4\xbe\xe5\x9c\x98, \xe8\x81\x9a\xe6\x9c\x83, \xe5\x9c\x98\xe9\xab\x94\x1b[12;62H[dreamwave/s\x1b[13;17H6 \xe8\xa6\x96\xe8\x81\xbd\xe5\x8a\x87\xe5\xa0\xb4 \xe5\x81\xb6\xe5\x83\x8f, \xe9\x9f\xb3\xe6\xa8\x82, \xe5\xbb\xa3\xe9\x9b\xbb\x1b[13;62H[mousepad\xe4\xbb\xa3]\x1b[14;17H7 \xe6\x88\xb0\xe7\x95\xa5\xe9\xab\x98\xe6\x89\x8b \xe9\x81\x8a\xe6\x88\xb2, \xe6\x95\xb8\xe4\xbd\x8d, \xe7\xa8\x8b\xe8\xa8\xad\x1b[14;62H[a3225737]\x1b[15;17H8 \xe5\x8d\xa1\xe6\xbc\xab\xe5\xa4\xa2\xe5\xb7\xa5\xe5\xbb\xa0 \xe5\x8d\xa1\xe9\x80\x9a, \xe6\xbc\xab\xe7\x95\xab, \xe5\x8b\x95\xe7\x95\xab\x1b[15;62H[hay955940/k\x1b[16;17H9 \xe7\x94\x9f\xe6\xb4\xbb\xe5\xa8\x9b\xe6\xa8\x82\xe9\xa4\xa8 \xe7\x94\x9f\xe6\xb4\xbb, \xe5\xa8\x9b\xe6\xa8\x82, \xe5\xbf\x83\xe6\x83\x85\x1b[16;62H[Bignana/sky\x1b[17;16H10 \xe5\x9c\x8b\xe5\xae\xb6\xe7\xa0\x94\xe7\xa9\xb6\xe9\x99\xa2 \xe6\x94\xbf\xe6\xb2\xbb, \xe6\x96\x87\xe5\xad\xb8, \xe5\xad\xb8\xe8\xa1\x93\x1b[17;62H[JosephChen/\x1b[18;16H11 \xe5\x9c\x8b\xe5\xae\xb6\xe9\xab\x94\xe8\x82\xb2\xe5\xa0\xb4 \xe6\xb1\x97\xe6\xb0\xb4, \xe9\xac\xa5\xe5\xbf\x97, \xe8\x86\xbd\xe8\xad\x98\x1b[18;62H[JUNstudio]\x1b[19;16H12 \x08\x08\x1b[1;31m\xcb\x87\x1b[19;21H\x1b[m[\xe4\xb8\x89\xe9\x87\x91] \xe9\x87\x91\xe9\xa6\xac58 \xe7\x80\x91\xe5\xb8\x83 \xe5\x9b\x9b\xe7\x8d\x8e\xe5\xa4\xa7\xe8\xb4\x8f\xe5\xae\xb6\x1b[19;62HFelix76116/s\x1b[20;16H13 \x08\x08\x1b[1;31m\xcb\x87\x1b[20;21H\x1b[m[\xe6\xad\xa6\xe6\xbc\xa2\xe8\x82\xba\xe7\x82\x8e] \xe5\x85\xa8\xe5\x9c\x8b\xe4\xba\x8c\xe7\xb4\x9a\xe8\xad\xa6\xe6\x88\x92\x1b[20;62Hswattw/flyin\x1b[21;16H14 \x08\x08\x1b[1;31m\xcb\x87\x1b[21;21H\x1b[m\xe3\x80\x8aQuestCenter\xe3\x80\x8b\xe7\x9c\x8b\xe6\x9d\xbf\xe8\xb3\x87\xe8\xa8\x8a\xe5\x85\xac\xe5\xb8\x83\xe6\xac\x84\x1b[21;62Hzhibb/inhuma\x1b[22;16H15 --> \xe5\x8d\xb3\xe6\x99\x82\xe7\x86\xb1\xe9\x96\x80\xe7\x9c\x8b\xe6\x9d\xbf <--\x1b[23;11H> 16 \x08\x08\x1b[1;31m\xcb\x87\x1b[23;21H\x1b[m[\xe6\xb4\xbb\xe5\x8b\x95] \xe6\x9c\x8d\xe5\x8b\x99\xe8\xad\x89\xe6\x9b\xb8\xe7\x94\xb3\xe8\xab\x8b 12/1-12/10\x1b[23;62HVal/chuo/han\x1b[23;11H\x1b[H\x1b[2J\x1b[1;37;44m\xe3\x80\x90\xe4\xb8\xbb\xe5\x8a\x9f\xe8\x83\xbd\xe8\xa1\xa8\xe3\x80\x91 \x1b[33m\xe6\x89\xb9\xe8\xb8\xa2\xe8\xb8\xa2\xe5\xaf\xa6\xe6\xa5\xad\xe5\x9d\x8a\x1b[0;1;37;44m \x1b[2;7H \x08\x08\x1b[0;1;37m\xe2\x97\x8f\x1b[2;9H\x1b[41m \x08\x08\xe2\x97\xa2\x1b[2;15H \x08\x08\x1b[47m\xe2\x97\xa4\x1b[2;17H \x08\x08\x1b[0;30;47m\xe2\x96\x85\x1b[2;19H \x08\x08\xe2\x96\x85\x1b[2;21H \x08\x08\x1b[m\xe2\x97\xa3\x1b[2;23H \x08\x08\x1b[1;37m\xe2\x97\xa2\x1b[2;31H \x08\x08\xe2\x96\x88\x1b[2;33H \x08\x08\xe2\x96\x88\x1b[2;35H \x08\x08\xe2\x96\x88\x1b[2;37H \x08\x08\xe2\x96\x88\x1b[2;39H \x08\x08\xe2\x96\x88\x1b[2;41H \x08\x08\xe2\x96\x88\x1b[2;43H \x08\x08\xe2\x96\x88\x1b[2;45H \x08\x08\xe2\x97\xa3\x1b[2;47H \x08\x08\xe2\x97\xa2\x1b[2;53H \x08\x08\xe2\x97\xa3\x1b[2;55H \x08\x08\xe2\x97\xa2\x1b[2;61H \x08\x08\xe2\x97\xa3\x1b[2;63H \x08\x08\xe2\x97\xa2\x1b[2;69H \x08\x08\xe2\x96\x88\x1b[2;71H \x08\x08\xe2\x96\x88\x1b[2;73H \x08\x08\xe2\x96\x88\x1b[2;75H \x08\x08\xe2\x96\x88\x1b[2;77H \x08\x08\xe2\x96\x88\x1b[2;79H\x1b[3;3H \x08\x08\xe2\x97\xa2\x1b[3;5H \x08\x08\xe2\x97\xa3\x1b[3;7H \x08\x08\x1b[0;31m\xe2\x97\xa5\x1b[3;11H \x08\x08\x1b[0;1;37;41m\xe2\x97\xa2\x1b[3;13H \x08\x08\x1b[47m\xe2\x97\xa4\x1b[3;15H \x08\x08\x1b[0;30;47m\xe2\x97\x8f\x1b[3;20H \x08\x08\x1b[0;1;31m\xe2\x97\xa3\x1b[3;25H \x08\x08\x1b[0;1;37m\xe2\x97\xa2\x1b[3;29H \x08\x08\x1b[46m\xe2\x97\xa4\x1b[3;31H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;33H \x08\x08\x1b[46m\xe2\x97\xa4\x1b[3;35H \x08\x08\xe2\x97\xa5\x1b[3;37H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;39H \x08\x08\x1b[46m\xe2\x97\xa5\x1b[3;41H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;43H \x08\x08\x1b[46m\xe2\x97\xa5\x1b[3;45H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;47H \x08\x08\xe2\x97\xa3\x1b[3;49H \x08\x08\xe2\x97\xa2\x1b[3;51H \x08\x08\xe2\x96\x88\x1b[3;53H \x08\x08\xe2\x96\x88\x1b[3;55H \x08\x08\xe2\x97\xa3\x1b[3;57H \x08\x08\xe2\x97\xa2\x1b[3;59H \x08\x08\xe2\x96\x88\x1b[3;61H \x08\x08\xe2\x96\x88\x1b[3;63H \x08\x08\xe2\x97\xa3\x1b[3;65H \x08\x08\xe2\x97\xa2\x1b[3;67H \x08\x08\x1b[46m\xe2\x97\xa4\x1b[3;69H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;71H \x08\x08\x1b[46m\xe2\x97\xa4\x1b[3;73H \x08\x08\xe2\x97\xa5\x1b[3;75H \x08\x08\x1b[0;1;37m\xe2\x96\x88\x1b[3;77H \x08\x08\x1b[46m\xe2\x97\xa5\x1b[3;79H\r\n \x08\x08\x1b[0;1;37m\xe2\x97\xa2\x1b[4;3H \x08\x08\xe2\x96\x88\x1b[4;5H \x08\x08\xe2\x96\x88\x1b[4;7H \x08\x08\xe2\x97\xa3\x1b[4;9H \x08\x08\x1b[47m\xe2\x97\xa4\x1b[4;13H \x1b[0;36m \x08\x08\xe2\x97\xa2\x1b[4;27H \x08\x08\x1b[0;1;37;46m\xe2\x97\xa4\x1b[4;29H \x08\x08\xe2\x97\xa4\x1b[4;33H \x08\x08\xe2\x97\xa5\x1b[4;39H \x08\x08\xe2\x97\xa5\x1b[4;43H \x08\x08\xe2\x97\xa5\x1b[4;47H \x08\x08\xe2\x97\xa5\x1b[4;49H \x08\x08\xe2\x97\xa4\x1b[4;51H \x08\x08\xe2\x97\xa4\x1b[4;53H \x08\x08\xe2\x97\xa4\x1b[4;55H \x08\x08\xe2\x97\xa5\x1b[4;57H \x08\x08\xe2\x97\xa4\x1b[4;59H \x08\x08\xe2\x97\xa4\x1b[4;61H \x08\x08\xe2\x97\xa4\x1b[4;63H \x08\x08\xe2\x97\xa5\x1b[4;65H \x08\x08\xe2\x97\xa4\x1b[4;67H \x08\x08\xe2\x97\xa4\x1b[4;71H \x08\x08\xe2\x97\xa5\x1b[4;77H \x08\x08\x1b[0;36m\xe2\x96\x88\x1b[4;79H\r\n \x08\x08\x1b[0;1;37;46m\xe2\x97\xa4\x1b[5;3H \x08\x08\xe2\x97\xa4\x1b[5;5H \x08\x08\xe2\x97\xa4\x1b[5;7H \x08\x08\xe2\x97\xa5\x1b[5;9H \x08\x08\x1b[0;36m\xe2\x97\xa3\x1b[5;11H \x08\x08\x1b[m\xe2\x97\xa5\x1b[5;13H\x1b[31;47m \x08\x08\xe2\x97\xa5\x1b[5;20H\x1b[0;41m_ \x08\x08\x1b[0;31m\xe2\x97\xa4\x1b[5;23H \x08\x08\x1b[36m\xe2\x97\xa2\x1b[5;25H\x1b[0;1;37;46m \x08\x08\xe2\x95\xad\x1b[5;31H \x08\x08\xe2\x95\xae\x1b[5;33H \x08\x08\xe2\x94\x82\x1b[5;35H \x08\x08\xe2\x95\xad\x1b[5;41H \x08\x08\xe2\x95\xae\x1b[5;43H \x08\x08\xe2\x94\x82\x1b[5;59H \x08\x08\xe2\x94\x82\x1b[5;77H \r\n\x1b[0;46m \x08\x08\x1b[0;36m\xe2\x97\xa4\x1b[6;11H \x08\x08\x1b[31m\xe2\x97\xa2\x1b[6;13H\x1b[41m \x1b[0;31m \x1b[0;1;37;46m \x08\x08\xe2\x94\x82\x1b[6;31H \x08\x08\xe2\x94\x82\x1b[6;33H \x08\x08\xe2\x94\x82\x1b[6;35H \x08\x08\xe2\x88\x95\x1b[6;37H\xe3\x80\x82 \x08\x08\xe2\x94\x82\x1b[6;41H \x08\x08\xe2\x94\x82\x1b[6;43H \x08\x08\xe2\x95\xad\x1b[6;45H \x08\x08\xe2\x95\xae\x1b[6;47H \x08\x08\xe2\x95\xad\x1b[6;49H \x08\x08\xe2\x95\xae\x1b[6;51H \x08\x08\xe2\x94\x82\x1b[6;53H \x08\x08\xe2\x94\x82\x1b[6;57H \x08\x08\xe2\x94\x9c\x1b[6;59H \x08\x08\xe2\x95\xae\x1b[6;61H \x08\x08\xe2\x95\xad\x1b[6;63H \x08\x08\xe2\x95\xae\x1b[6;65H \x08\x08\xe2\x95\xad\x1b[6;67H \x08\x08\xe2\x95\xae\x1b[6;69H \x08\x08\xe2\x95\xad\x1b[6;71H \x08\x08\xe2\x95\xad\x1b[6;73H \x08\x08\xe2\x95\xad\x1b[6;75H \x08\x08\xe2\x94\xa4\x1b[6;77H \x1b[7;9H \x08\x08\x1b[m\xe2\x97\xa2\x1b[7;11H \x08\x08\x1b[31;47m\xe2\x97\xa2\x1b[7;13H \x08\x08\xe2\x97\xa4\x1b[7;15H \x08\x08\x1b[30m\xe2\x95\xb2\x1b[7;17H_ \x08\x08\x1b[m\xe2\x97\xa3\x1b[7;23H \x08\x08\x1b[1;37m\xe2\x95\xb0\x1b[7;31H \x08\x08\xe2\x95\xae\x1b[7;33H \x08\x08\xe2\x94\x9c\x1b[7;35H \x08\x08\xe2\x95\xae\x1b[7;37H \x08\x08\xe2\x94\x82\x1b[7;39H \x08\x08\xe2\x95\xb0\x1b[7;41H \x08\x08\xe2\x95\xae\x1b[7;43H \x08\x08\xe2\x94\x82\x1b[7;45H \x08\x08\xe2\x94\x82\x1b[7;47H \x08\x08\xe2\x94\x82\x1b[7;49H \x08\x08\xe2\x94\x82\x1b[7;51H \x08\x08\xe2\x94\x82\x1b[7;53H \x08\x08\xe2\x94\x82\x1b[7;55H \x08\x08\xe2\x94\x82\x1b[7;57H \x08\x08\xe2\x94\x82\x1b[7;59H \x08\x08\xe2\x94\x82\x1b[7;61H \x08\x08\xe2\x94\x82\x1b[7;63H \x08\x08\xe2\x94\x82\x1b[7;65H \x08\x08\xe2\x95\xad\x1b[7;67H \x08\x08\xe2\x94\xa4\x1b[7;69H \x08\x08\xe2\x94\x9c\x1b[7;71H \x08\x08\xe2\x95\xaf\x1b[7;73H \x08\x08\xe2\x94\x82\x1b[7;75H \x08\x08\xe2\x94\x82\x1b[7;77H\r\n \x08\x08\x1b[30m\xe2\x95\xad\x1b[8;3H \x08\x08\xe2\x94\x80\x1b[8;5H \x08\x08\xe2\x94\x80\x1b[8;7H \x08\x08\xe2\x94\x80\x1b[8;9H \x08\x08\x1b[0;31;47m\xe2\x97\xa2\x1b[8;11H \x08\x08\xe2\x97\xa4\x1b[8;13H \x08\x08\x1b[30m\xe2\x97\xa5\x1b[8;20H \x08\x08\xe2\x96\x8e\x1b[8;22H \x1b[0;30m \x1b[0;1;37;44m \x08\x08\xe2\x94\x82\x1b[8;31H \x08\x08\xe2\x94\x82\x1b[8;33H \x08\x08\xe2\x94\x82\x1b[8;35H \x08\x08\xe2\x94\x82\x1b[8;37H \x08\x08\xe2\x94\x82\x1b[8;41H \x08\x08\xe2\x94\x82\x1b[8;43H \x08\x08\xe2\x94\x82\x1b[8;45H \x08\x08\xe2\x94\x82\x1b[8;47H \x08\x08\xe2\x95\xb0\x1b[8;49H \x08\x08\xe2\x95\xaf\x1b[8;51H \x08\x08\xe2\x95\xb0\x1b[8;53H \x08\x08\xe2\x94\xb4\x1b[8;55H \x08\x08\xe2\x95\xaf\x1b[8;57H \x08\x08\xe2\x94\x9c\x1b[8;59H \x08\x08\xe2\x95\xaf\x1b[8;61H \x08\x08\xe2\x95\xb0\x1b[8;63H \x08\x08\xe2\x95\xaf\x1b[8;65H \x08\x08\xe2\x95\xb0\x1b[8;67H \x08\x08\xe2\x94\xa4\x1b[8;69H \x08\x08\xe2\x94\x82\x1b[8;71H \x08\x08\xe2\x95\xb0\x1b[8;75H \x08\x08\xe2\x94\xa4\x1b[8;77H \r\n \x08\x08\x1b[0;1;30m\xe2\x95\xb0\x1b[9;3H \x08\x08\xe2\x94\x80\x1b[9;5H \x08\x08\xe2\x94\x80\x1b[9;7H \x08\x08\xe2\x94\x80\x1b[9;9H\x1b[0;30;47m ____ \x08\x08\xe2\x95\xb1\x1b[9;20H \x1b[0;30m \x1b[0;1;37;44m \x08\x08\xe2\x95\xb0\x1b[9;31H \x08\x08\xe2\x95\xaf\x1b[9;33H \x08\x08\xe2\x94\x82\x1b[9;37H \x08\x08\xe2\x95\xb0\x1b[9;41H \x08\x08\xe2\x95\xaf\x1b[9;43H \r\n \x08\x08\x1b[0;34m\xe2\x96\x84\x1b[10;3H \x08\x08\xe2\x96\x84\x1b[10;5H \x08\x08\xe2\x96\x84\x1b[10;7H \x1b[30;47m \x08\x08\xe2\x97\xa2\x1b[10;15H \x08\x08\xe2\x97\xa4\x1b[10;17H \x1b[0;30m \x1b[0;1;37;44m \x08\x08\xe2\x94\x82\x1b[10;37H \x1b[36m\xe6\xad\xa1 \xe8\xbf\x8e \xe5\xa4\xa7 \xe5\xae\xb6 \xe4\xbe\x86 \xe6\xbb\x91 \xe9\x9b\xaa \xe6\x9d\xbf \xe9\x80\x9b \xe9\x80\x9b \xe5\x93\xa6 ! \x1b[11;9H \x08\x08\x1b[m\xe2\x97\xa5\x1b[11;11H \x08\x08\x1b[30;47m\xe2\x97\xa2\x1b[11;13H \x08\x08\xe2\x97\xa4\x1b[11;15H \x08\x08\x1b[m\xe2\x97\xa4\x1b[11;23H \x08\x08\x1b[33m\xe2\x97\xa2\x1b[11;25H\x1b[43m \x1b[0;33m \x1b[44m \x1b[12;2H\x1b[1;30;43mby fuxk \x08\x08\x1b[0;30;43m\xe2\x97\xa2\x1b[12;11H \x08\x08\xe2\x97\xa4\x1b[12;13H\x1b[1mfuxk fuxk \x08\x08\x1b[0;33m\xe2\x97\xa4\x1b[12;25H \x1b[1;44m\xe3\x80\x90\xe5\x9c\x8b\xe5\xae\xb6\xe9\xab\x94\xe8\x82\xb2\xe5\xa0\xb4\xe3\x80\x91 \x08\x08\xe2\x86\x92\x1b[12;46H \xe3\x80\x90PttSport\xe3\x80\x91 \x08\x08\xe2\x86\x92\x1b[12;62H \xe3\x80\x90SkiSnowboard\xe3\x80\x91\x1b[13;23H\x1b[m(\x1b[1;36mA\x1b[m)nnounce \xe3\x80\x90 \xe7\xb2\xbe\xe8\x8f\xaf\xe5\x85\xac\xe4\xbd\x88\xe6\xac\x84 \xe3\x80\x91\x1b[14;23H(\x1b[1;36mF\x1b[m)avorite \xe3\x80\x90 \xe6\x88\x91 \xe7\x9a\x84 \xe6\x9c\x80\xe6\x84\x9b \xe3\x80\x91\x1b[15;23H(\x1b[1;36mC\x1b[m)lass\x1b[15;38H\xe3\x80\x90 \xe5\x88\x86\xe7\xb5\x84\xe8\xa8\x8e\xe8\xab\x96\xe5\x8d\x80 \xe3\x80\x91\x1b[16;23H(\x1b[1;36mM\x1b[m)ail\x1b[16;38H\xe3\x80\x90 \xe7\xa7\x81\xe4\xba\xba\xe4\xbf\xa1\xe4\xbb\xb6\xe5\x8d\x80 \xe3\x80\x91\x1b[17;23H(\x1b[1;36mT\x1b[m)alk\x1b[17;38H\xe3\x80\x90 \xe4\xbc\x91\xe9\x96\x92\xe8\x81\x8a\xe5\xa4\xa9\xe5\x8d\x80 \xe3\x80\x91\x1b[18;23H(\x1b[1;36mU\x1b[m)ser\x1b[18;38H\xe3\x80\x90 \xe5\x80\x8b\xe4\xba\xba\xe8\xa8\xad\xe5\xae\x9a\xe5\x8d\x80 \xe3\x80\x91\x1b[19;23H(\x1b[1;36mX\x1b[m)yz\x1b[19;38H\xe3\x80\x90 \xe7\xb3\xbb\xe7\xb5\xb1\xe8\xb3\x87\xe8\xa8\x8a\xe5\x8d\x80 \xe3\x80\x91\x1b[20;23H(\x1b[1;36mP\x1b[m)lay\x1b[20;38H\xe3\x80\x90 \xe5\xa8\x9b\xe6\xa8\x82\xe8\x88\x87\xe4\xbc\x91\xe9\x96\x92 \xe3\x80\x91\x1b[21;23H(\x1b[1;36mN\x1b[m)amelist \xe3\x80\x90 \xe7\xb7\xa8\xe7\x89\xb9\xe5\x88\xa5\xe5\x90\x8d\xe5\x96\xae \xe3\x80\x91\x1b[22;21H> (\x1b[1;36mG\x1b[m)oodbye\x1b[22;41H\xe9\x9b\xa2\xe9\x96\x8b\xef\xbc\x8c\xe5\x86\x8d\xe8\xa6\x8b \x08\x08\xe2\x80\xa6\x1b[22;53H\r\n\n\x1b[34;46m[12/4 \xe6\x98\x9f\xe6\x9c\x9f\xe5\x85\xad 16:24]\x1b[1;33;45m [ \xe5\xb0\x84\xe6\x89\x8b\xe6\x99\x82 ] \x1b[30;47m \xe7\xb7\x9a\xe4\xb8\x8a\x1b[31m66945\x1b[30m\xe4\xba\xba, \xe6\x88\x91\xe6\x98\xaf\x1b[31mCodingMan\x1b[30m [\xe5\x91\xbc\xe5\x8f\xab\xe5\x99\xa8]\x1b[31m\xe9\x97\x9c\xe9\x96\x89 \x1b[m\x1b[22;21H'
# query post
screen = b'\x1b[H\x1b[2J\x1b[1;30m\xe3\x80\x90\xe6\x9d\xbf\xe4\xb8\xbb:catcatcatcat\xe3\x80\x91 Python \xe7\xa8\x8b\xe5\xbc\x8f\xe8\xaa\x9e\xe8\xa8\x80 \xe7\x9c\x8b\xe6\x9d\xbf\xe3\x80\x8aPython\xe3\x80\x8b \r\n[ \x08\x08\xe2\x86\x90\x1b[2;4H]\xe9\x9b\xa2\xe9\x96\x8b [ \x08\x08\xe2\x86\x92\x1b[2;13H]\xe9\x96\xb1\xe8\xae\x80 [Ctrl-P]\xe7\x99\xbc\xe8\xa1\xa8\xe6\x96\x87\xe7\xab\xa0 [d]\xe5\x88\xaa\xe9\x99\xa4 [z]\xe7\xb2\xbe\xe8\x8f\xaf\xe5\x8d\x80 [i]\xe7\x9c\x8b\xe6\x9d\xbf\xe8\xb3\x87\xe8\xa8\x8a/\xe8\xa8\xad\xe5\xae\x9a [h]\xe8\xaa\xaa\xe6\x98\x8e \r\n\n\x1b[0;1;37m> 1 112/09 ericsk \x08\x08\xe2\x96\xa1\x1b[4;33H [\xe5\xbf\x83\xe5\xbe\x97] \xe7\xb5\x82\xe6\x96\xbc\xe9\x96\x8b\xe6\x9d\xbf\xe4\xba\x86 \r\n \x08\x08\x1b[m\xe2\x94\x8c\x1b[5;3H \x08\x08\xe2\x94\x80\x1b[5;5H \x08\x08\xe2\x94\x80\x1b[5;7H \x08\x08\xe2\x94\x80\x1b[5;9H \x08\x08\xe2\x94\x80\x1b[5;11H \x08\x08\xe2\x94\x80\x1b[5;13H \x08\x08\xe2\x94\x80\x1b[5;15H \x08\x08\xe2\x94\x80\x1b[5;17H \x08\x08\xe2\x94\x80\x1b[5;19H \x08\x08\xe2\x94\x80\x1b[5;21H \x08\x08\xe2\x94\x80\x1b[5;23H \x08\x08\xe2\x94\x80\x1b[5;25H \x08\x08\xe2\x94\x80\x1b[5;27H \x08\x08\xe2\x94\x80\x1b[5;29H \x08\x08\xe2\x94\x80\x1b[5;31H \x08\x08\xe2\x94\x80\x1b[5;33H \x08\x08\xe2\x94\x80\x1b[5;35H \x08\x08\xe2\x94\x80\x1b[5;37H \x08\x08\xe2\x94\x80\x1b[5;39H \x08\x08\xe2\x94\x80\x1b[5;41H \x08\x08\xe2\x94\x80\x1b[5;43H \x08\x08\xe2\x94\x80\x1b[5;45H \x08\x08\xe2\x94\x80\x1b[5;47H \x08\x08\xe2\x94\x80\x1b[5;49H \x08\x08\xe2\x94\x80\x1b[5;51H \x08\x08\xe2\x94\x80\x1b[5;53H \x08\x08\xe2\x94\x80\x1b[5;55H \x08\x08\xe2\x94\x80\x1b[5;57H \x08\x08\xe2\x94\x80\x1b[5;59H \x08\x08\xe2\x94\x80\x1b[5;61H \x08\x08\xe2\x94\x80\x1b[5;63H \x08\x08\xe2\x94\x80\x1b[5;65H \x08\x08\xe2\x94\x80\x1b[5;67H \x08\x08\xe2\x94\x80\x1b[5;69H \x08\x08\xe2\x94\x80\x1b[5;71H \x08\x08\xe2\x94\x80\x1b[5;73H \x08\x08\xe2\x94\x80\x1b[5;75H \x08\x08\xe2\x94\x80\x1b[5;77H \x08\x08\xe2\x94\x90\x1b[5;79H\r\n \x08\x08\xe2\x94\x82\x1b[6;3H \xe6\x96\x87\xe7\xab\xa0\xe4\xbb\xa3\xe7\xa2\xbc(AID): \x1b[1;37m#13cPSYOX \x1b[m(Python) [ptt.cc] [\xe5\xbf\x83\xe5\xbe\x97] \xe7\xb5\x82\xe6\x96\xbc\xe9\x96\x8b\xe6\x9d\xbf\xe4\xba\x86\x1b[6;77H \x08\x08\xe2\x94\x82\x1b[6;79H\r\n \x08\x08\xe2\x94\x82\x1b[7;3H \xe6\x96\x87\xe7\xab\xa0\xe7\xb6\xb2\xe5\x9d\x80: \x1b[1;37mhttps://www.ptt.cc/bbs/Python/M.1134139170.A.621.html\x1b[7;77H \x08\x08\x1b[m\xe2\x94\x82\x1b[7;79H\r\n \x08\x08\xe2\x94\x82\x1b[8;3H \xe9\x80\x99\xe4\xb8\x80\xe7\xaf\x87\xe6\x96\x87\xe7\xab\xa0\xe5\x80\xbc 2 Ptt\xe5\xb9\xa3\x1b[8;77H \x08\x08\xe2\x94\x82\x1b[8;79H\r\n \x08\x08\xe2\x94\x94\x1b[9;3H \x08\x08\xe2\x94\x80\x1b[9;5H \x08\x08\xe2\x94\x80\x1b[9;7H \x08\x08\xe2\x94\x80\x1b[9;9H \x08\x08\xe2\x94\x80\x1b[9;11H \x08\x08\xe2\x94\x80\x1b[9;13H \x08\x08\xe2\x94\x80\x1b[9;15H \x08\x08\xe2\x94\x80\x1b[9;17H \x08\x08\xe2\x94\x80\x1b[9;19H \x08\x08\xe2\x94\x80\x1b[9;21H \x08\x08\xe2\x94\x80\x1b[9;23H \x08\x08\xe2\x94\x80\x1b[9;25H \x08\x08\xe2\x94\x80\x1b[9;27H \x08\x08\xe2\x94\x80\x1b[9;29H \x08\x08\xe2\x94\x80\x1b[9;31H \x08\x08\xe2\x94\x80\x1b[9;33H \x08\x08\xe2\x94\x80\x1b[9;35H \x08\x08\xe2\x94\x80\x1b[9;37H \x08\x08\xe2\x94\x80\x1b[9;39H \x08\x08\xe2\x94\x80\x1b[9;41H \x08\x08\xe2\x94\x80\x1b[9;43H \x08\x08\xe2\x94\x80\x1b[9;45H \x08\x08\xe2\x94\x80\x1b[9;47H \x08\x08\xe2\x94\x80\x1b[9;49H \x08\x08\xe2\x94\x80\x1b[9;51H \x08\x08\xe2\x94\x80\x1b[9;53H \x08\x08\xe2\x94\x80\x1b[9;55H \x08\x08\xe2\x94\x80\x1b[9;57H \x08\x08\xe2\x94\x80\x1b[9;59H \x08\x08\xe2\x94\x80\x1b[9;61H \x08\x08\xe2\x94\x80\x1b[9;63H \x08\x08\xe2\x94\x80\x1b[9;65H \x08\x08\xe2\x94\x80\x1b[9;67H \x08\x08\xe2\x94\x80\x1b[9;69H \x08\x08\xe2\x94\x80\x1b[9;71H \x08\x08\xe2\x94\x80\x1b[9;73H \x08\x08\xe2\x94\x80\x1b[9;75H \x08\x08\xe2\x94\x80\x1b[9;77H \x08\x08\xe2\x94\x98\x1b[9;79H\r\n\n\x1b[1;30m 8 12/10 Fenikso \x08\x08\xe2\x96\xa1\x1b[11;33H \xe8\xb3\x80 \r\n 9 12/10 asf423 \x08\x08\xe2\x96\xa1\x1b[12;33H \xe8\xb3\x80\xe9\x96\x8b\xe7\x89\x88 \r\n 10 12/10 rofu \x08\x08\xe2\x96\xa1\x1b[13;33H \xe8\xb3\x80\xe9\x96\x8b\xe7\x89\x88 \r\n 11 12/10 NewYork \x08\x08\xe2\x96\xa1\x1b[14;33H \xe8\xb3\x80 \r\n 12 12/10 abacada \x08\x08\xe2\x96\xa1\x1b[15;33H \xe6\x81\xad\xe5\x96\x9c\xe9\x96\x8b\xe6\x9d\xbf \r\n 13 12/10 jftsai \x08\x08\xe2\x96\xa1\x1b[16;33H \xe6\x81\xad\xe5\x96\x9c \r\n 14 12/11 aceace \x08\x08\xe2\x96\xa1\x1b[17;33H \xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe8\xbb\x8a\xe4\xbe\x86\xe4\xba\x86 \r\n 15 12/11 polaristin \x08\x08\xe2\x96\xa1\x1b[18;33H \xe8\xb3\x80\xe9\x96\x8b\xe6\x9d\xbf XD \r\n 16 12/11 jingel \x08\x08\xe2\x96\xa1\x1b[19;33H \xe6\x81\xad\xe5\x96\x9c\xe9\x96\x8b\xe6\x9d\xbf \r\n 17 12/11 milen \x08\x08\xe2\x96\xa1\x1b[20;33H \xe6\x81\xad\xe5\x96\x9c\xe9\x96\x8b\xe6\x9d\xbf!!~~^^~~ \r\n 18 12/12 littlebear \x08\x08\xe2\x96\xa1\x1b[21;33H \xe6\x81\xad\xe5\x96\x9c\xe9\x96\x8b\xe6\x9d\xbf...........^^^^ \r\n 19 12/12 zhouer \x08\x08\xe2\x96\xa1\x1b[22;33H [\xe9\x87\x8e\xe4\xba\xba\xe7\x8d\xbb\xe6\x9b\x9d] List Comprehensions \r\n 20 12/12 tippy \x08\x08\xe2\x96\xa1\x1b[23;33H [\xe5\xbf\x83\xe5\xbe\x97] httplib \r\n\x1b[34;44m \x08\x08\xe2\x96\x84\x1b[24;4H \x08\x08\xe2\x96\x84\x1b[24;6H \x08\x08\xe2\x96\x84\x1b[24;8H \x08\x08\xe2\x96\x84\x1b[24;10H \x08\x08\xe2\x96\x84\x1b[24;12H \x08\x08\xe2\x96\x84\x1b[24;14H \x08\x08\xe2\x96\x84\x1b[24;16H \x08\x08\xe2\x96\x84\x1b[24;18H \x08\x08\xe2\x96\x84\x1b[24;20H \x08\x08\xe2\x96\x84\x1b[24;22H \x08\x08\xe2\x96\x84\x1b[24;24H \x08\x08\xe2\x96\x84\x1b[24;26H \x08\x08\xe2\x96\x84\x1b[24;28H \x08\x08\xe2\x96\x84\x1b[24;30H \x08\x08\xe2\x96\x84\x1b[24;32H\x1b[0;1;37;44m \xe8\xab\x8b\xe6\x8c\x89\xe4\xbb\xbb\xe6\x84\x8f\xe9\x8d\xb5\xe7\xb9\xbc\xe7\xba\x8c \x08\x08\x1b[34m\xe2\x96\x84\x1b[24;50H \x08\x08\xe2\x96\x84\x1b[24;52H \x08\x08\xe2\x96\x84\x1b[24;54H \x08\x08\xe2\x96\x84\x1b[24;56H \x08\x08\xe2\x96\x84\x1b[24;58H \x08\x08\xe2\x96\x84\x1b[24;60H \x08\x08\xe2\x96\x84\x1b[24;62H \x08\x08\xe2\x96\x84\x1b[24;64H \x08\x08\xe2\x96\x84\x1b[24;66H \x08\x08\xe2\x96\x84\x1b[24;68H \x08\x08\xe2\x96\x84\x1b[24;70H \x08\x08\xe2\x96\x84\x1b[24;72H \x08\x08\xe2\x96\x84\x1b[24;74H \x08\x08\xe2\x96\x84\x1b[24;76H \x08\x08\xe2\x96\x84\x1b[24;78H \x1b[m'
# test post
screen = b'\x1b[H\x1b[2J\x1b[24;1H\n\x1b[K\x1b[H\n\xe5\xa6\x82\xe6\x9e\x9c\xe4\xbd\xa0\xe9\x82\x84\xe4\xb8\x8d\xe7\x9f\xa5\xe9\x81\x93\xe5\xa6\x82\xe4\xbd\x95\xe4\xbd\xbf\xe7\x94\xa8 Python \xe6\x93\x8d\xe4\xbd\x9c PTT\r\n\xe9\x82\xa3 PTT Library \xe6\x9c\x83\xe6\x98\xaf\xe4\xbd\xa0\xe4\xb8\x8d\xe9\x8c\xaf\xe7\x9a\x84\xe9\x81\xb8\xe6\x93\x87\xe3\x80\x82\r\n\nPTT Library \xe6\x95\xb4\xe7\x90\x86\xe4\xba\x86\xe7\xb6\xb2\xe8\xb7\xaf\xe4\xb8\x8a\xe8\xb7\x9f PTT \xe6\x89\x93\xe4\xba\xa4\xe9\x81\x93\xe7\x9a\x84\xe7\xa8\x8b\xe5\xbc\x8f\xe7\xa2\xbc\r\n\xe6\x9c\x9f\xe6\x9c\x9b\xe5\x9c\xa8 Python \xe5\x8f\xaf\xe4\xbb\xa5\xe6\x8f\x90\xe4\xbe\x9b\xe4\xb8\x80\xe5\x80\x8b\xe7\xa9\xa9\xe5\xae\x9a\xe5\xae\x8c\xe6\x95\xb4\xe7\x9a\x84\xe6\x9c\x8d\xe5\x8b\x99\r\n\n\xe5\xa6\x82\xe6\x9e\x9c\xe4\xbd\xa0\xe6\x9c\x89 PTT Library \xe5\xb0\x9a\xe6\x9c\xaa\xe6\x94\xb6\xe8\x97\x8f\xe7\x9a\x84\xe5\x8a\x9f\xe8\x83\xbd\r\n\xe6\xad\xa1\xe8\xbf\x8e\xe6\x8f\x90\xe5\x87\xba pull request :D\r\n\n\xe4\xbb\xa5\xe4\xb8\x8b\xe6\x98\xaf\xe6\x88\x91\xe6\x9c\x80\xe8\xbf\x91\xe6\x94\xb9\xe7\x89\x88\xe7\x9a\x84\xe9\x87\x8d\xe9\xbb\x9e\r\n\n1. \xe6\x9e\xb6\xe6\xa7\x8b\xe6\x89\x93\xe6\x8e\x89\xe9\x87\x8d\xe7\xb7\xb4\xef\xbc\x8c\xe4\xbb\xa5\xe6\x8f\x90\xe5\x8d\x87\xe9\x96\xb1\xe8\xae\x80\xe8\x88\x87\xe7\xb6\xad\xe8\xad\xb7\xe6\x80\xa7\xef\xbc\x8c\xe7\xac\xa6\xe5\x90\x88 PEP8 \xe9\xa2\xa8\xe6\xa0\xbc\xe8\xa6\x8f\xe7\xaf\x84\r\n2. \xe6\x94\xaf\xe6\x8f\xb4\xe6\x9c\x80\xe6\x96\xb0 WebSocket \xe9\x80\xa3\xe7\xb7\x9a\xe6\xa8\xa1\xe5\xbc\x8f\r\n3. \xe6\x94\xaf\xe6\x8f\xb4\xe5\xa4\x9a\xe5\x9c\x8b\xe8\xaa\x9e\xe7\xb3\xbb\xef\xbc\x8c\xe8\x8b\xb1\xe6\x96\x87\xe8\x88\x87\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87\r\n4. \xe6\x96\xbc Windows 10, Ubuntu 18.04 and MacOS 10.14 \xe6\xb8\xac\xe8\xa9\xa6\r\n5. \xe4\xbf\xae\xe6\xad\xa3\xe5\x9c\xa8\xe5\x89\x8d\xe4\xb8\x80\xe7\x89\x88\xe6\x94\xb6\xe9\x9b\x86\xe5\x88\xb0\xe7\x9a\x84\xe5\x95\x8f\xe9\xa1\x8c\r\n\ngithub: https://github.com/Truth0906/PTTLibrary\r\n\n\xe5\xa6\x82\xe6\x9e\x9c\xe4\xbd\xa0\xe5\x96\x9c\xe6\xad\xa1\xef\xbc\x8c\xe6\xad\xa1\xe8\xbf\x8e\xe7\xb5\xa6\xe6\x88\x91\xe5\x80\x8b\xe6\x98\x9f\xe6\x98\x9f :D\r\n--\r\n \x08\x08\x1b[32m\xe2\x80\xbb\x1b[23;3H \xe7\x99\xbc\xe4\xbf\xa1\xe7\xab\x99: \xe6\x89\xb9\xe8\xb8\xa2\xe8\xb8\xa2\xe5\xaf\xa6\xe6\xa5\xad\xe5\x9d\x8a(ptt.cc), \xe4\xbe\x86\xe8\x87\xaa: 111.243.146.98 (\xe8\x87\xba\xe7\x81\xa3)\r\n\x1b[34;46m \xe7\x80\x8f\xe8\xa6\xbd \xe7\xac\xac 1/2 \xe9\xa0\x81 ( 34%) \x1b[1;30;47m \xe7\x9b\xae\xe5\x89\x8d\xe9\xa1\xaf\xe7\xa4\xba: \xe7\xac\xac 06~28 \xe8\xa1\x8c\x1b[0;47m \x1b[31m(y)\x1b[30m\xe5\x9b\x9e\xe6\x87\x89\x1b[31m(X%)\x1b[30m\xe6\x8e\xa8\xe6\x96\x87\x1b[31m(h)\x1b[30m\xe8\xaa\xaa\xe6\x98\x8e\x1b[31m( \x08\x08\xe2\x86\x90\x1b[24;74H)\x1b[30m\xe9\x9b\xa2\xe9\x96\x8b \x1b[m'
p = VT100Parser(screen, 'utf-8')
print(p.screen)
================================================
FILE: PyPtt/service.py
================================================
import threading
import time
import uuid
from typing import Optional
from . import PTT
from . import check_value
from . import log
class Service:
def __init__(self, pyptt_init_config: Optional[dict] = None):
"""
這是一個可以在多執行緒中使用的 PyPtt API 服務。
| 請注意:這僅僅只是 Thread Safe 的實作,對效能並不會有實質上的幫助。
| 如果你需要更好的效能,請在每一個線程都使用一個 PyPtt.API 本身。
Args:
pyptt_init_config (dict): PyPtt 初始化設定,請參考 :ref:`初始化設定 `。
Returns:
None
範例::
from PyPtt import Service
def api_test(thread_id, service):
result = service.call('get_time')
print(f'thread id {thread_id}', 'get_time', result)
result = service.call('get_aid_from_url', {'url': 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html'})
print(f'thread id {thread_id}', 'get_aid_from_url', result)
result = service.call('get_newest_index', {'index_type': PyPtt.NewIndex.BOARD, 'board': 'Python'})
print(f'thread id {thread_id}', 'get_newest_index', result)
if __name__ == '__main__':
pyptt_init_config = {
# 'language': PyPtt.Language.ENGLISH,
}
service = Service(pyptt_init_config)
try:
service.call('login', {'ptt_id': 'YOUR_PTT_ID', 'ptt_pw': 'YOUR_PTT_PW'})
pool = []
for i in range(10):
t = threading.Thread(target=api_test, args=(i, service))
t.start()
pool.append(t)
for t in pool:
t.join()
service.call('logout')
finally:
service.close()
"""
if pyptt_init_config is None:
pyptt_init_config = {}
log_level = pyptt_init_config.get('log_level', log.INFO)
self.logger = log.init(log_level, 'service')
self.logger.info('init')
self._api = None
self._api_init_config = pyptt_init_config
self._call_queue = []
self._call_result = {}
self._id_pool = set()
self._id_pool_lock = threading.Lock()
self._close = False
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
while self._api is None:
time.sleep(0.01)
def _run(self):
if self._api is not None:
self._api.logout()
self._api = None
self._api = PTT.API(**self._api_init_config)
self.logger.info('start')
while not self._close:
if len(self._call_queue) == 0:
time.sleep(0.05)
continue
call = self._call_queue.pop(0)
func = getattr(self._api, call['api'])
api_result = None
api_exception = None
try:
api_result = func(**call['args'])
except Exception as e:
api_exception = e
self._call_result[call['id']] = {
'result': api_result,
'exception': api_exception
}
def _get_call_id(self):
while True:
call_id = uuid.uuid4().hex
with self._id_pool_lock:
if call_id not in self._id_pool:
self._id_pool.add(call_id)
return call_id
def call(self, api: str, args: Optional[dict] = None):
if args is None:
args = {}
check_value.check_type(api, str, 'api')
check_value.check_type(args, dict, 'args')
if api not in dir(self._api):
raise ValueError(f'api {api} not found')
call = {
'api': api,
'id': self._get_call_id(),
'args': args
}
self._call_queue.append(call)
while call['id'] not in self._call_result:
time.sleep(0.01)
call_result = self._call_result[call['id']]
del self._call_result[call['id']]
with self._id_pool_lock:
self._id_pool.remove(call['id'])
if call_result['exception'] is not None:
raise call_result['exception']
return call_result['result']
def close(self):
self.logger.info('close')
self._close = True
self._thread.join()
self.logger.info('done')
================================================
FILE: README.md
================================================

# PyPtt
[](https://pypi.python.org/pypi/PyPtt)

[](https://github.com/PyPtt/PyPtt/actions/workflows/test.yml)

[](https://www.gnu.org/licenses/lgpl-3.0)
[](https://t.me/PyPtt)
[](http://paypal.me/CodingMan)
#### PyPtt (PTT Library) 是一套 Pure Python PTT API 是目前支援最完整的 PTT API。具備大部分常用功能,無論推文、發文、取得文章、取得信件、寄信、發 P 幣、丟水球,你都可以在這裡找到完整的使用範例
#### 使用帳號登入,支援使用登入之後才可以使用的功能,例如:推文、發文、寄信、發 P 幣等等
#### 本專案意旨在提供 PTT 自動化機器人函式庫,無意違反任何 PTT 站方規範。如有牴觸,請馬上告知。
####
#### Pypi: https://pypi.org/project/PyPtt/
## 安裝
```bash
pip install PyPtt
```
## 回報問題
#### 請參考 [常見問題](https://pyptt.cc/faq.html) 章節
## 加入 PyPtt 社群
#### 你可以在 Telegram 上找到 PyPtt 社群 [](https://t.me/PyPtt)
## 贊助
#### 如果這個專案對你有幫助,贊助我一杯咖啡吧!!
####
#### [](http://paypal.me/CodingMan)
## 贊助清單
#### leftc
================================================
FILE: docs/CNAME
================================================
pyptt.cc
================================================
FILE: docs/Makefile
================================================
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
================================================
FILE: docs/api/bucket.rst
================================================
bucket
==========
.. _api-bucket:
.. automodule:: PyPtt.API
:members: bucket
:noindex:
================================================
FILE: docs/api/change_pw.rst
================================================
change_pw
==============
.. automodule:: PyPtt.API
:members: change_pw
:noindex:
================================================
FILE: docs/api/comment.rst
================================================
comment
==========
.. _api-comment:
.. automodule:: PyPtt.API
:members: comment
:noindex:
================================================
FILE: docs/api/del_mail.rst
================================================
del_mail
============
.. _api-del-mail:
.. automodule:: PyPtt.API
:members: del_mail
:noindex:
================================================
FILE: docs/api/del_post.rst
================================================
del_post
==============
.. automodule:: PyPtt.API
:members: del_post
:noindex:
================================================
FILE: docs/api/get_aid_from_url.rst
================================================
get_aid_from_url
=====================
.. automodule:: PyPtt.API
:members: get_aid_from_url
:noindex:
================================================
FILE: docs/api/get_all_boards.rst
================================================
get_all_boards
=================
.. automodule:: PyPtt.API
:members: get_all_boards
:noindex:
================================================
FILE: docs/api/get_board_info.rst
================================================
get_board_info
=================
.. _api-get-board-info:
.. automodule:: PyPtt.API
:members: get_board_info
:noindex:
================================================
FILE: docs/api/get_bottom_post_list.rst
================================================
get_bottom_post_list
======================
.. automodule:: PyPtt.API
:members: get_bottom_post_list
:noindex:
================================================
FILE: docs/api/get_favourite_boards.rst
================================================
get_favourite_boards
==========================
.. _api-get-favourite-boards:
.. automodule:: PyPtt.API
:members: get_favourite_boards
:noindex:
================================================
FILE: docs/api/get_mail.rst
================================================
get_mail
============
.. _api-get-mail:
.. automodule:: PyPtt.API
:members: get_mail
:noindex:
================================================
FILE: docs/api/get_newest_index.rst
================================================
get_newest_index
=================
.. _api-get-newest-index:
.. automodule:: PyPtt.API
:members: get_newest_index
:noindex:
================================================
FILE: docs/api/get_post.rst
================================================
get_post
==========
.. _api-get-post:
.. automodule:: PyPtt.API
:members: get_post
:noindex:
================================================
FILE: docs/api/get_time.rst
================================================
get_time
==========
.. _api-get-time:
.. automodule:: PyPtt.API
:members: get_time
:noindex:
================================================
FILE: docs/api/get_user.rst
================================================
get_user
==========
.. _api-get-user:
.. automodule:: PyPtt.API
:members: get_user
:noindex:
================================================
FILE: docs/api/give_money.rst
================================================
give_money
=============
.. _api-give-money:
.. automodule:: PyPtt.API
:members: give_money
:noindex:
================================================
FILE: docs/api/index.rst
================================================
APIs
=============
| 這是 PyPtt 的 API 文件。
| 我們在這裡介紹 PyPtt 目前所有支援 PTT, PTT2 的功能。
基本功能
----------------
.. toctree::
init
login_logout
文章相關
----------------
.. toctree::
get_post
get_newest_index
post
reply_post
del_post
comment
信箱相關
----------------
.. toctree::
mail
get_mail
del_mail
使用者相關
----------------
.. toctree::
give_money
get_user
search_user
change_pw
取得 PTT 資訊
-------------------
.. toctree::
get_time
get_all_boards
get_favourite_boards
get_board_info
get_aid_from_url
get_bottom_post_list
版主相關
----------------
.. toctree::
set_board_title
mark_post
bucket
================================================
FILE: docs/api/init.rst
================================================
init
=======
.. _api-init:
.. automodule:: PyPtt.API
:members: __init__
================================================
FILE: docs/api/login_logout.rst
================================================
login, logout
================
.. _api-login-logout:
.. automodule:: PyPtt.API
:members: login, logout
:noindex:
================================================
FILE: docs/api/mail.rst
================================================
mail
=============
.. _api-mail:
.. automodule:: PyPtt.API
:members: mail
:noindex:
================================================
FILE: docs/api/mark_post.rst
================================================
mark_post
===============
.. _api-mark-post:
.. automodule:: PyPtt.API
:members: mark_post
:noindex:
================================================
FILE: docs/api/post.rst
================================================
post
==========
.. _api-post:
.. automodule:: PyPtt.API
:members: post
:noindex:
================================================
FILE: docs/api/reply_post.rst
================================================
reply_post
==========
.. _api-reply-post:
.. automodule:: PyPtt.API
:members: reply_post
:noindex:
================================================
FILE: docs/api/search_user.rst
================================================
search_user
================
.. _api-search-user:
.. automodule:: PyPtt.API
:members: search_user
:noindex:
================================================
FILE: docs/api/set_board_title.rst
================================================
set_board_title
=====================
.. _api-set-board-title:
.. automodule:: PyPtt.API
:members: set_board_title
:noindex:
================================================
FILE: docs/changelog.rst
================================================
更新日誌
====================
| 這裡寫著 PyPtt 的故事。
|
| 2022.12.20 PyPtt 1.0.3,logger 改採用以 logging_ 為基底。
.. _logging: https://docs.python.org/3/howto/logging.html
| 2022.12.19 發佈 :doc:`Docker Image `。
| 2022.12.08 PyPtt 1.0.1, 1.0.2,修正一些小錯誤
| 2022.12.08 PyPtt 1.0.0 正式發布。
| 2021.12.08 PyPtt 新增 :doc:`service` 功能。
| 2022.12.01 開發 頁面改名為 Roadmap。
| 2022.09.19 更換主題為 furo_。
.. _furo: https://sphinx-themes.org/sample-sites/furo/
| 2022.09.14 太棒了!我們終於有更新日誌了。
================================================
FILE: docs/conf.py
================================================
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
import os
import sys
from datetime import datetime
sys.path.insert(0, os.path.abspath('../'))
import PyPtt
project = 'PyPtt'
copyright = f'2017 ~ {datetime.now().year}, CodingMan'
author = 'CodingMan'
version = PyPtt.__version__
release = PyPtt.__version__
html_title = f'PyPtt.cc'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx_copybutton',
'sphinx.ext.autosectionlabel',
'sphinx_sitemap',
]
autosectionlabel_prefix_document = True
html_baseurl = 'https://pyptt.cc/'
sitemap_url_scheme = "{link}"
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
language = 'zh_TW'
html_extra_path = ['CNAME', 'robots.txt']
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'furo'
html_static_path = ['_static']
html_favicon = "https://raw.githubusercontent.com/PyPtt/PyPtt/master/logo/facebook_profile_image.png"
================================================
FILE: docs/dev.rst
================================================
Development
================
如果你想參與開發,請參考以下須知:
開發環境
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
我們建議您使用 virtualenv 來建立獨立的 Python 環境,以避免相依性問題。
.. code-block:: bash
virtualenv venv
source venv/bin/activate
安裝相依套件
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
你可以使用以下指令來安裝相依套件:
.. code-block:: bash
pip install -r requirements.txt
如果你想更改文件,請安裝開發相依套件:
.. code-block:: bash
pip install -r docs/requirements.txt
產生文件網頁
.. code-block:: bash
bash make_doc.sh
你可以在 docs/_build/html/index.html 中查看文件網頁。
執行測試
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
你可以使用以下指令來執行測試:
.. code-block:: python
python3 tests/*.py
如果有遺漏的測試,請不吝發起 Pull Request。
撰寫文件
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 如果你的變更涉及文件,請記得更新文件。
| 我們使用 Sphinx 來撰寫文件,你可以在 docs/ 中找到文件的原始碼。
建立你的 Pull Request
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
如果你想要貢獻程式碼,請參考以下步驟:
1. Fork 這個專案。
2. 建立你的特性分支 (`git checkout -b feat/my-new-feature`)。
3. Commit 你的變更 (`git commit -am 'feat: add some feature`)。
commit msg 格式,請參考 `Conventional Commits`_。
4. Push 到你的分支 (`git push origin feat/my-new-feature`)。
5. 建立一個新的 Pull Request。
請注意,我們會優先處理符合 `Conventional Commits`_ 的 Pull Request。
.. _Conventional Commits: https://www.conventionalcommits.org/en/v1.0.0/
================================================
FILE: docs/docker.rst
================================================
Docker Image
=================
.. image:: https://img.shields.io/docker/v/codingman000/pyptt/latest
:target: https://hub.docker.com/r/codingman000/pyptt
.. image:: https://img.shields.io/docker/pulls/codingman000/pyptt?color=orange
:target: https://hub.docker.com/r/codingman000/pyptt
.. image:: https://img.shields.io/docker/image-size/codingman000/pyptt/latest?color=green
:target: https://hub.docker.com/r/codingman000/pyptt
.. image:: https://img.shields.io/docker/stars/codingman000/pyptt?color=succes
:target: https://hub.docker.com/r/codingman000/pyptt
| 是的,PyPtt 也支援 Docker Image。
| 只要一行指令就可以啟動一個 PyPtt 的 Docker Image,並且可以在 Docker Image 中使用 PyPtt。
|
| Doc: https://pyptt.cc/docker.html
| Docker hub: https://hub.docker.com/r/codingman000/pyptt
| Github: https://github.com/PyPtt/PyPtt_image
安裝
-----------------
.. code-block:: bash
docker pull codingman000/pyptt:latest
啟動
-----------------
.. code-block:: bash
docker run -d -p 8787:8787 codingman000/pyptt:latest
連線
-----------------
物件編碼的方法你可以在這裏了解 程式碼_
.. _程式碼: https://github.com/PyPtt/PyPtt_image/blob/main/src/utils.py#L4
.. code-block:: python
import PyPtt
import requests
from src.utils import object_encode
from tests import config
if __name__ == '__main__':
params = {
"api": "login",
"args": object_encode({
'ptt_id': config.PTT_ID,
'ptt_pw': config.PTT_PW
})
}
r = requests.get("http://localhost:8787/api", params=params)
print(r.json())
params = {
"api": "get_time",
}
r = requests.get("http://localhost:8787/api", params=params)
print(r.json())
params = {
"api": "get_newest_index",
"args": object_encode({
'board': 'Gossiping',
'index_type': PyPtt.NewIndex.BOARD
})
}
r = requests.get("http://localhost:8787/api", params=params)
print(r.json())
##############################
content = """此內容由 PyPtt image 執行 PO 文
測試換行 123
測試換行 456
測試換行 789
"""
params = {
"api": "post",
"args": object_encode({
'board': 'Test',
'title_index': 1,
'title': 'test',
'content': content,
})
}
r = requests.get("http://localhost:8787/api", params=params)
print(r.json())
##############################
params = {
"api": "logout",
}
r = requests.get("http://localhost:8787/api", params=params)
print(r.json())
================================================
FILE: docs/examples.rst
================================================
使用範例
=============
| 這裡記錄了各種實際使用的範例 ☺️
保持登入
--------
這裡示範了如何保持登入
.. code-block:: python
import PyPtt
def login():
max_retry = 5
ptt_bot = None
for retry_time in range(max_retry):
try:
ptt_bot = PyPtt.API()
ptt_bot.login('YOUR_ID', 'YOUR_PW',
kick_other_session=False if retry_time == 0 else True)
break
except PyPtt.exceptions.LoginError:
ptt_bot = None
print('登入失敗')
time.sleep(3)
except PyPtt.exceptions.LoginTooOften:
ptt_bot = None
print('請稍後再試')
time.sleep(60)
except PyPtt.exceptions.WrongIDorPassword:
print('帳號密碼錯誤')
raise
except Exception as e:
print('其他錯誤:', e)
break
return ptt_bot
if __name__ == '__main__':
login()
last_newest_index = ptt_bot.get_newest_index()
time.sleep(60)
try:
while True:
try:
newest_index = ptt_bot.get_newest_index()
except PyPtt.exceptions.ConnectionClosed:
ptt_bot = login()
continue
except Exception as e:
print('其他錯誤:', e)
break
if newest_index == last_newest_index:
continue
print('有新文章!', newest_index)
# do something
time.sleep(5)
finally:
ptt_bot.logout()
幫你的文章上色
--------------
如果在發的時候有上色的需求,可以透過模擬鍵盤輸入的方式達到加上色碼的效果
.. code-block:: python
import PyPtt
content = [
PTT.command.Ctrl_C + PTT.command.Left + '5' + PTT.command.Right + '這是閃爍字' + PTT.command.Ctrl_C,
PTT.command.Ctrl_C + PTT.command.Left + '31' + PTT.command.Right + '前景紅色' + PTT.command.Ctrl_C,
PTT.command.Ctrl_C + PTT.command.Left + '44' + PTT.command.Right + '背景藍色' + PTT.command.Ctrl_C,
]
content = '\n'.join(content)
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content=content, sign_file=0)
finally:
ptt_bot.logout()
.. image:: _static/color_demo.png
.. _check_post_status:
如何判斷文章資料是否可以使用
------------------------------
當 :doc:`api/get_post` 回傳文章資料回來時,這時需要一些判斷來決定是否要使用這些資料
.. code-block:: python
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
post_info = ptt_bot.get_post('Python', index=1)
print(post_info)
if post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.EXISTS:
print('文章存在!')
elif post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.DELETED_BY_AUTHOR:
print('文章被作者刪除')
sys.exit()
elif post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.DELETED_BY_MODERATOR:
print('文章被版主刪除')
sys.exit()
if not post_info[PyPtt.PostField.pass_format_check]:
print('未通過格式檢查')
sys.exit()
print('文章資料可以使用')
finally:
ptt_bot.logout()
================================================
FILE: docs/exceptions.rst
================================================
例外
========
| 這裡介紹 PyPtt 的例外。
| 可以用 try...except... 來處理。
| 例外的種類
.. py:exception:: PyPtt.exceptions.RequireLogin
:module: PyPtt
需要登入。
.. py:exception:: PyPtt.exceptions.NoPermission
:module: PyPtt
沒有權限。
.. py:exception:: PyPtt.exceptions.LoginError
:module: PyPtt
登入失敗。
.. py:exception:: PyPtt.exceptions.NoFastComment
:module: PyPtt
無法快速推文。
.. py:exception:: PyPtt.exceptions.NoSuchUser
:module: PyPtt
查無此使用者。
.. py:exception:: PyPtt.exceptions.NoSuchMail
:module: PyPtt
查無此信件。
.. py:exception:: PyPtt.exceptions.NoMoney
:module: PyPtt
餘額不足。
.. py:exception:: PyPtt.exceptions.NoSuchBoard
:module: PyPtt
查無此看板。
.. py:exception:: PyPtt.exceptions.ConnectionClosed
:module: PyPtt
連線已關閉。
.. py:exception:: PyPtt.exceptions.UnregisteredUser
:module: PyPtt
未註冊使用者。
.. py:exception:: PyPtt.exceptions.MultiThreadOperated
:module: PyPtt
同時使用多個 thread 呼叫 PyPtt 。
.. py:exception:: PyPtt.exceptions.WrongIDorPassword
:module: PyPtt
帳號或密碼錯誤。
.. py:exception:: PyPtt.exceptions.WrongPassword
:module: PyPtt
密碼錯誤。
.. py:exception:: PyPtt.exceptions.LoginTooOften
:module: PyPtt
登入太頻繁。
.. py:exception:: PyPtt.exceptions.UseTooManyResources
:module: PyPtt
使用過多資源。
.. py:exception:: PyPtt.exceptions.HostNotSupport
:module: PyPtt
主機不支援。詳見 :ref:`host`。
.. py:exception:: PyPtt.exceptions.CantComment
:module: PyPtt
禁止推文。
.. py:exception:: PyPtt.exceptions.CantResponse
:module: PyPtt
已結案並標記, 不得回應。
.. py:exception:: PyPtt.exceptions.NeedModeratorPermission
:module: PyPtt
需要版主權限。
.. py:exception:: PyPtt.exceptions.ConnectError
:module: PyPtt
連線失敗。
.. py:exception:: PyPtt.exceptions.NoSuchPost
:module: PyPtt
文章不存在。
.. py:exception:: PyPtt.exceptions.CanNotUseSearchPostCode
:module: PyPtt
無法使用搜尋文章代碼。
.. py:exception:: PyPtt.exceptions.UserHasPreviouslyBeenBanned
:module: PyPtt
`水桶`_ 使用者,但已經被 `水桶`_。
.. py:exception:: PyPtt.exceptions.MailboxFull
:module: PyPtt
信箱已滿。
.. py:exception:: PyPtt.exceptions.NoSearchResult
:module: PyPtt
搜尋結果為空。
.. py:exception:: PyPtt.exceptions.OnlySecureConnection
:module: PyPtt
只能使用安全連線。
.. py:exception:: PyPtt.exceptions.SetContactMailFirst
:module: PyPtt
請先設定聯絡信箱。
.. py:exception:: PyPtt.exceptions.ResetYourContactEmail
:module: PyPtt
請重新設定聯絡信箱。
.. _水桶: https://pttpedia.fandom.com/zh/wiki/%E6%B0%B4%E6%A1%B6
================================================
FILE: docs/faq.rst
================================================
FAQ
==========
這裡搜集了一些常見問題的解答,如果你有任何問題,請先看看這裡。
Q: 我該如何使用 PyPtt?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| A: 可以先參考 :doc:`install`、:doc:`api/index` 與 :doc:`examples`。
Q: 使用 PyPtt 時,遇到問題該如何解決?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1. 自己修正並提交 PR,如果沒辦法請參考步驟 2。
* 如果你是程式設計師,可以參考 :doc:`參與開發 ` 一起幫忙修正問題。
2. 到 GitHub 提出 `issue`_ 或者到 `PyPtt Telegram 社群`_ 討論。
* 請先確認你使用的版本是否為 |version_pic|,如果不是,請更新到最新版本。
* 如果你使用的是最新版本,請確認你的問題是否已經在這裡被回答過了。
* 如果你的問題還沒有被回答過,請依照以下程式碼將 LogLevel_ 設定為 `DEBUG`,並附上 **可以重現問題的程式碼**。
* 到 GitHub 提出 `issue`_ 或者到 `PyPtt Telegram 社群`_ 討論。
.. code-block:: python
import PyPtt
ptt_bot = PyPtt.API(log_level=PyPtt.LogLevel.DEBUG)
# 你的程式碼
.. |version_pic| image:: https://img.shields.io/pypi/v/PyPtt.svg
:target: https://pypi.org/project/PyPtt/
.. _`PyPtt Telegram 社群`: https://t.me/PyPtt
.. _LogLevel: https://github.com/PttCodingMan/SingleLog/blob/d7c19a1b848dfb1c9df8201f13def9a31afd035c/SingleLog/SingleLog.py#L22
.. _`issue`: https://github.com/PyPtt/PyPtt/issues/new
Q: 在 jupyter 遭遇 `the event loop is already running` 錯誤
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| A: 因為 jupyter 內部也使用了 asyncio 作為協程管理工具,會跟 PyPtt 內部的 asyncio 衝突,所以如果想要在 jypyter 內使用,請在你的程式碼中加入以下程式碼
.. code-block:: bash
:caption: 安裝 nest_asyncio
! pip install nest_asyncio
.. code-block:: python
:caption: 在程式碼中引用 nest_asyncio
import nest_asyncio
nest_asyncio.apply()
Q: 在 Mac 無法使用 WebSocket 連線,遭遇 SSL 相關錯誤
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| A: 請參考以下指令,安裝 Python 的 SSL 憑證
.. code-block:: bash
:caption: 以 Python 3.10 為例
sh /Applications/Python\ 3.10/Install\ Certificates.command
Q: 為什麼我沒辦法在雲端環境上使用 PyPtt?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| A: 如果你是使用雲端 (Colab、GCP、Azure、AWS...etc) 無法連線 PTT 是正常的。
| 因為 PTT 有防止機器人登入的機制,所以在雲端環境上無法使用 PyPtt。
================================================
FILE: docs/index.rst
================================================
PyPtt
====================
.. image:: _static/logo_cover.png
:alt: PyPtt: PTT bot library for Python
:align: center
.. image:: https://img.shields.io/pypi/v/PyPtt.svg
:target: https://pypi.org/project/PyPtt/
.. image:: https://img.shields.io/github/last-commit/pyptt/pyptt.svg?color=green
:target: https://github.com/PyPtt/PyPtt/commits/
.. image:: https://img.shields.io/pypi/dm/PyPtt?color=ocean
:target: https://pypi.org/project/PyPtt/
.. image:: https://github.com/PyPtt/PyPtt/actions/workflows/test.yml/badge.svg?branch=master&color=yellogreen
:target: https://github.com/PyPtt/PyPtt/actions/workflows/test.yml
.. image:: https://img.shields.io/pypi/pyversions/PyPtt
:target: https://pypi.org/project/PyPtt/
.. image:: https://img.shields.io/badge/License-LGPL%20v3-blue.svg
:target: https://www.gnu.org/licenses/lgpl-3.0
.. image:: https://img.shields.io/github/stars/pyptt/pyptt?style=social
:target: https://github.com/PyPtt/PyPtt/stargazers
| PyPtt_ 是時下最流行的 PTT library,你可以在 Python 程式碼裡面使用 PTT 常見的操作,例如::doc:`推文 `、:doc:`發文 `、:doc:`寄信 `、:doc:`讀取信件 `、:doc:`讀取文章 ` 等等操作。
|
| 本文件的內容會隨著 PyPtt_ 的更新而更新,如果你發現任何錯誤,歡迎到 PyPtt_ 發 issue 或者加入 `PyPtt Telegram 社群`_ 一起討論。
|
| PyPtt 由 CodingMan_ 與其他許多的 `貢獻者`_ 共同維護。
.. _PyPtt: https://github.com/PyPtt/PyPtt
.. _`PyPtt Telegram 社群`: https://t.me/PyPtt
.. _CodingMan: https://github.com/PttCodingMan
.. _`貢獻者`: https://github.com/PyPtt/PyPtt/graphs/contributors
重要消息
--------------------
| 2022.12.19 發佈 :doc:`Docker Image `。
| 2022.12.08 PyPtt 1.0.0 正式發布
| 2021.12.08 PyPtt 新增 :doc:`service` 功能
文件
----------------
:doc:`安裝 PyPtt `
如何把 PyPtt 安裝到你的環境中。
:doc:`APIs `
PyPtt 的所有 API 說明。
:doc:`Service `
如何在多線程的情況,安全地使用 PyPtt。
:doc:`參數型態 `
PyPtt 的所有參數型態選項。
:doc:`例外 `
PyPtt 所有你可能遭遇到的錯誤。
:doc:`使用範例 `
一些使用 PyPtt 的範例。
:doc:`Docker Image `
如何使用 Docker Image 來使用 PyPtt。
:doc:`參與開發 `
如果你想要貢獻 PyPtt,可以看看這裡。
:doc:`常見問題 `
任何常見問題都可以在這找到解答。
:doc:`Roadmap `
| 這裡列了我們正在做什麼與打算做什麼。
| 如果你想要貢獻 PyPtt,可以看看這裡。
:doc:`ChangeLog `
| 我們曾經做了什麼。
.. toctree::
:maxdepth: 3
:caption: 目錄
:hidden:
install
api/index
service
type
exceptions
examples
docker image
參與開發
常見問題
Roadmap
ChangeLog
Source Code
PyPI
================================================
FILE: docs/install.rst
================================================
安裝 PyPtt
===================
Python 版本
--------------
| 推薦使用 CPython_ 3.8+。
.. _CPython: https://www.python.org/
相依套件
--------------
PyPtt 目前相依於以下套件,這些套件都會在安裝的過程中被自動安裝。
* progressbar2_ is a text progress bar library for Python.
* websockets_ is a library for building WebSocket_ servers and clients in Python with a focus on correctness, simplicity, robustness, and performance.
* uao_ is a pure Python implementation of the Unicode encoder/decoder.
* requests_ is a Python HTTP library, released under the Apache License 2.0.
* AutoStrEnum_ is a Python library that provides an Enum class that automatically converts enum values to and from strings.
* PyYAML_ is a YAML parser and emitter for Python.
.. _progressbar2: https://progressbar-2.readthedocs.io/en/latest/
.. _websockets: https://websockets.readthedocs.io/en/stable/
.. _`WebSocket`: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
.. _uao: https://github.com/eight04/pyUAO
.. _requests: https://requests.readthedocs.io/en/master/
.. _AutoStrEnum: https://github.com/PttCodingMan/PttCodingMan
.. _PyYAML: https://pyyaml.org/
使用虛擬環境安裝 (推薦)
-------------------------
| 我們推薦各位使用虛擬環境 venv_ 來安裝 PyPtt,因為可以盡可能地避免套件衝突。
|
| 你可以從 `Virtual Environments and Packages`_ 中了解,更多關於使用虛擬環境的理由以及如何建立你的虛擬環境。
.. _`Virtual Environments and Packages`: https://docs.python.org/3/tutorial/venv.html#tut-venv
.. _venv: https://docs.python.org/3/library/venv.html
安裝指令
----------------
你可以使用以下指令來安裝 PyPtt。
.. code-block:: bash
pip install PyPtt
現在 PyPtt 已經成功安裝了,來看看 PyPtt 的 :doc:`API 說明 ` 或者 :doc:`使用範例 ` 吧!
================================================
FILE: docs/make.bat
================================================
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
================================================
FILE: docs/requirements.txt
================================================
PyPtt
sphinx
sphinx-copybutton
pygments==2.15.0
Furo
sphinx-sitemap
================================================
FILE: docs/roadmap.rst
================================================
開發
=============
| 這裡列了一些我們正在開發的功能,如果你有任何建議,歡迎找我們聊聊。
| 或者你也可以直接在 github 上開 issue,我們會盡快回覆。
| 當然如果你有興趣參與開發,也歡迎你加入我們,我們會盡快回覆你的加入申請。
未來開發計劃
--------------------
* WebSocket 支援 Tor or Proxy
期待有一天可以透過 Tor 或 Proxy 來連接到 PTT,讓 PyPtt 可以自由地在雲端伺服器運作。
開發中
--------------------
* 支援 PTT 官方 APP API
你可以在 ptt-app-api_ 分支上找到目前的進度。
.. _ptt-app-api: https://github.com/PyPtt/PyPtt/tree/ptt-app-api
已完成
--------------------
* PyPtt Service docker
| 期待 PyPtt 在未來可以有 API 形式的服務,讓大家可以透過 API 呼叫來使用 PyPtt。
| 這樣其實在某個層面也上可以達到使用 Tor or Proxy 的目的。
* PyPtt :doc:`service` 2022.12.18 完成
* Docker :doc:`docker` 2022.12.19 完成
* 官方網站的建置 2022.12.18 完成
使用 sphinx 來建置官方網站,讓大家可以更方便地了解 PyPtt。
* 測試案例 2022.12.15 完成
* 1.0 正式版本重構 2022.11.15 完成
================================================
FILE: docs/robots.txt
================================================
User-agent: *
Sitemap: https://pyptt.cc/sitemap.xml
================================================
FILE: docs/service.rst
================================================
Service
===========
.. automodule:: PyPtt.Service
:members: __init__
:undoc-members:
================================================
FILE: docs/type.rst
================================================
參數型態
===========
這裡介紹 PyPtt 的參數型態
.. _host:
HOST
-----------
* 連線的 PTT 伺服器。
.. py:attribute:: PyPtt.HOST.PTT1
批踢踢實業坊
.. py:attribute:: PyPtt.HOST.PTT2
批踢踢兔
.. _language:
Language
-----------
* 顯示訊息的語言。
.. py:attribute:: PyPtt.Language.MANDARIN
繁體中文
.. py:attribute:: PyPtt.Language.ENGLISH
英文
.. _connect-mode:
ConnectMode
-----------
* 連線的模式。
.. py:attribute:: PyPtt.ConnectMode.WEBSOCKETS
使用 WEBSOCKETS 連線
.. py:attribute:: PyPtt.ConnectMode.TELNET
使用 TELNET 連線
.. _new-index:
NewIndex
-----------
* 搜尋 Index 的種類。
.. py:attribute:: PyPtt.NewIndex.BOARD
搜尋看板 Index
.. py:attribute:: PyPtt.NewIndex.MAIL
搜尋信箱 Index
.. _search-type:
SearchType
-----------
* 搜尋看板的方式。
.. py:attribute:: PyPtt.SearchType.KEYWORD
搜尋關鍵字
.. py:attribute:: PyPtt.SearchType.AUTHOR
搜尋作者
.. py:attribute:: PyPtt.SearchType.COMMENT
搜尋推文數
.. py:attribute:: PyPtt.SearchType.MARK
搜尋標記
.. py:attribute:: PyPtt.SearchType.MONEY
搜尋稿酬
.. _reply-to:
ReplyTo
-----------
* 回文的方式。
.. py:attribute:: PyPtt.ReplyTo.BOARD
回文至看板
.. py:attribute:: PyPtt.ReplyTo.MAIL
回文至信箱
.. py:attribute:: PyPtt.ReplyTo.BOARD_MAIL
回文至看板與信箱
.. _comment-type:
CommentType
-----------
* 推文方式。
.. py:attribute:: PyPtt.CommentType.PUSH
推
.. py:attribute:: PyPtt.CommentType.BOO
噓
.. py:attribute:: PyPtt.CommentType.ARROW
箭頭
.. _post-status:
PostStatus
-----------
* 文章狀態。
.. py:attribute:: PyPtt.PostStatus.EXISTS
文章存在
.. py:attribute:: PyPtt.PostStatus.DELETED_BY_AUTHOR
被作者刪除
.. py:attribute:: PyPtt.PostStatus.DELETED_BY_MODERATOR
被板主刪除
.. py:attribute:: PyPtt.PostStatus.DELETED_BY_UNKNOWN
無法判斷,被如何刪除
.. _mark-type:
MarkType
-----------
* 版主標記文章種類
.. py:attribute:: PyPtt.MarkType.S
S 文章
.. py:attribute:: PyPtt.MarkType.D
標記文章
.. py:attribute:: PyPtt.MarkType.DELETE_D
刪除標記文章
.. py:attribute:: PyPtt.MarkType.M
M 起來
.. py:attribute:: PyPtt.MarkType.UNCONFIRMED
待證實文章
.. _user-field:
UserField
-----------
* 使用者資料欄位。
.. py:attribute:: PyPtt.UserField.ptt_id
使用者 ID
.. py:attribute:: PyPtt.UserField.money
經濟狀態
.. py:attribute:: PyPtt.UserField.login_count
登入次數
.. py:attribute:: PyPtt.UserField.account_verified
是否通過認證
.. py:attribute:: PyPtt.UserField.legal_post
文章數量
.. py:attribute:: PyPtt.UserField.illegal_post
退文數量
.. py:attribute:: PyPtt.UserField.activity
目前動態
.. py:attribute:: PyPtt.UserField.mail
信箱狀態
.. py:attribute:: PyPtt.UserField.last_login_date
最後登入時間
.. py:attribute:: PyPtt.UserField.last_login_ip
最後登入 IP
.. py:attribute:: PyPtt.UserField.five_chess
五子棋戰積
.. py:attribute:: PyPtt.UserField.chess
象棋戰積
.. py:attribute:: PyPtt.UserField.signature_file
簽名檔
.. _comment-field:
CommentField
--------------
* 推文資料欄位。
.. py:attribute:: PyPtt.CommentField.type
推文類型,推噓箭頭,詳見 :ref:`comment-type`
.. py:attribute:: PyPtt.CommentField.author
推文作者
.. py:attribute:: PyPtt.CommentField.content
推文內容
.. py:attribute:: PyPtt.CommentField.ip
推文 IP (如果存在)
.. py:attribute:: PyPtt.CommentField.time
推文時間
.. _favorite-board-field:
FavouriteBoardField
--------------------
* 我的最愛資料欄位。
.. py:attribute:: PyPtt.FavouriteBoardField.board
看板名稱
.. py:attribute:: PyPtt.FavouriteBoardField.title
看板標題
.. py:attribute:: PyPtt.FavouriteBoardField.type
類別
.. _mail-field:
MailField
----------
* 信件資料欄位。
.. py:attribute:: PyPtt.MailField.origin_mail
原始信件全文
.. py:attribute:: PyPtt.MailField.author
信件作者
.. py:attribute:: PyPtt.MailField.title
信件標題
.. py:attribute:: PyPtt.MailField.date
信件日期
.. py:attribute:: PyPtt.MailField.content
信件內容
.. py:attribute:: PyPtt.MailField.ip
信件 IP
.. py:attribute:: PyPtt.MailField.location
信件位置
.. py:attribute:: PyPtt.MailField.is_red_envelope
是否為紅包
.. _board-field:
BoardField
-----------
* 看板資料欄位。
.. py:attribute:: PyPtt.BoardField.board
看板名稱
.. py:attribute:: PyPtt.BoardField.online_user
在線人數
.. py:attribute:: PyPtt.BoardField.chinese_des
看板中文名稱
.. py:attribute:: PyPtt.BoardField.moderators
看板板主清單
.. py:attribute:: PyPtt.BoardField.open_status
看板公開狀態,是否隱板
.. py:attribute:: PyPtt.BoardField.into_top_ten_when_hide
隱板時是否可以進入十大排行榜
.. py:attribute:: PyPtt.BoardField.can_non_board_members_post
非看板成員是否可以發文
.. py:attribute:: PyPtt.BoardField.can_reply_post
是否可以回覆文章
.. py:attribute:: PyPtt.BoardField.self_del_post
是否可以自刪文章
.. py:attribute:: PyPtt.BoardField.can_comment_post
是否可以推文
.. py:attribute:: PyPtt.BoardField.can_boo_post
是否可以噓文
.. py:attribute:: PyPtt.BoardField.can_fast_push
是否可以快速推文
.. py:attribute:: PyPtt.BoardField.min_interval_between_comments
推文間隔時間
.. py:attribute:: PyPtt.BoardField.is_comment_record_ip
是否記錄推文 IP
.. py:attribute:: PyPtt.BoardField.is_comment_aligned
推文是否對齊
.. py:attribute:: PyPtt.BoardField.can_moderators_del_illegal_content
板主是否可以刪除違規文字
.. py:attribute:: PyPtt.BoardField.does_tran_post_auto_recorded_and_require_post_permissions
是否自動記錄轉錄文章並需要發文權限
.. py:attribute:: PyPtt.BoardField.is_cool_mode
是否為冷板模式
.. py:attribute:: PyPtt.BoardField.is_require18
是否為 18 禁看板
.. py:attribute:: PyPtt.BoardField.require_login_time
發文需要登入次數
.. py:attribute:: PyPtt.BoardField.require_illegal_post
發文需要最低退文數量
.. py:attribute:: PyPtt.BoardField.post_kind_list
發文類別,例如 [公告] [問卦] 等
.. _post-field:
PostField
-----------
* 文章資料欄位。
.. py:attribute:: PyPtt.PostField.board
文章所在看板
.. py:attribute:: PyPtt.PostField.aid
文章 ID,例如:`#1Z69g2ts`
.. py:attribute:: PyPtt.PostField.index
文章編號,例如:906
.. py:attribute:: PyPtt.PostField.author
文章作者
.. py:attribute:: PyPtt.PostField.date
文章日期
.. py:attribute:: PyPtt.PostField.title
文章標題
.. py:attribute:: PyPtt.PostField.content
文章內容
.. py:attribute:: PyPtt.PostField.money
文章稿酬,P 幣
.. py:attribute:: PyPtt.PostField.url
文章網址
.. py:attribute:: PyPtt.PostField.ip
文章 IP
.. py:attribute:: PyPtt.PostField.comments
文章推文清單,詳見 :ref:`comment-field`
.. py:attribute:: PyPtt.PostField.post_status
文章狀態,詳見 :ref:`post-status`
.. py:attribute:: PyPtt.PostField.list_date
文章列表日期
.. py:attribute:: PyPtt.PostField.has_control_code
文章是否有控制碼
.. py:attribute:: PyPtt.PostField.pass_format_check
文章是否通過格式檢查
.. py:attribute:: PyPtt.PostField.location
文章 IP 位置
.. py:attribute:: PyPtt.PostField.push_number
文章推文數量
.. py:attribute:: PyPtt.PostField.is_lock
文章是否鎖定
.. py:attribute:: PyPtt.PostField.full_content
文章完整內容
.. py:attribute:: PyPtt.PostField.is_unconfirmed
文章是否為未確認文章
================================================
FILE: make_doc.sh
================================================
make -C docs/ clean
make -C docs/ html
================================================
FILE: requirements.txt
================================================
progressbar2
websockets
uao
requests==2.31.0
AutoStrEnum
PyYAML
================================================
FILE: scripts/lang.py
================================================
import json
import os
import sys
from collections import defaultdict
import yaml
sys.path.append(os.getcwd())
import PyPtt
def add_lang():
new_words = [
(PyPtt.Language.MANDARIN, 'give_money', '給 _target0_ _target_ P 幣'),
(PyPtt.Language.ENGLISH, 'give_money', 'give _target0_ _target_ P coins'),
]
for lang, key, value in new_words:
PyPtt.i18n.init(lang, cache=True)
PyPtt.i18n._lang_data[key] = value
with open(f'PyPtt/lang/{lang}.yaml', 'w', encoding='utf-8') as f:
yaml.dump(PyPtt.i18n._lang_data, f, allow_unicode=True, default_flow_style=False)
def check_lang():
import re
# 搜尋 PyPtt 資料夾底下,所有用到 i18n 的字串
PyPtt.i18n.init(PyPtt.Language.MANDARIN, cache=True)
# init count dict
count_dict = {}
for key, value in PyPtt.i18n._lang_data.items():
print('->', key, value)
count_dict[key] = 0
# 1. 用 os.walk() 搜尋所有檔案
for dirpath, dirnames, filenames in os.walk('./PyPtt'):
print(f'================= directory: {dirpath}')
for file_name in filenames:
if not file_name.endswith('.py'):
continue
if file_name == 'i18n.py':
continue
print(file_name)
with open(f'{dirpath}/{file_name}', 'r', encoding='utf-8') as f:
data = f.read()
for match in re.finditer(r'i18n\.(\w+)', data):
# print(match.group(0))
# print(match.group(1))
data_key = match.group(1)
if data_key not in count_dict:
print(f'Unknown key: {data_key}')
else:
count_dict[data_key] += 1
print('-----------------')
print(json.dumps(count_dict, indent=4, ensure_ascii=False))
# collect the keys with 0 count
zero_count_keys = [key for key, value in count_dict.items() if value == 0]
for lang in PyPtt.i18n.locale_pool:
PyPtt.i18n.init(lang, cache=True)
for key in zero_count_keys:
# remove the key from the lang data
PyPtt.i18n._lang_data.pop(key, None)
print(f'Removed key: {key} from {lang}.yaml')
with open(f'PyPtt/lang/{lang}.yaml', 'w', encoding='utf-8') as f:
yaml.dump(PyPtt.i18n._lang_data, f, allow_unicode=True, default_flow_style=False)
if __name__ == '__main__':
add_lang()
# check_lang()
pass
================================================
FILE: scripts/package_script.py
================================================
import os
import subprocess
import time
def get_next_version():
is_merged = os.environ.get('GITHUB_EVENT_NAME') == 'pull_request' and os.environ.get(
'GITHUB_EVENT_ACTION') == 'closed'
print('is_merged:', is_merged)
# read the main version from __init__.py
with open('PyPtt/__init__.py', 'r', encoding='utf-8') as f:
data = f.read().strip()
main_version = data.split('_main_version = ')[1].split('\n')[0].strip().strip('\'')
print('main_version version:', main_version)
version = None
pypi_version = None
for i in range(5):
try:
# Use wget to retrieve the PyPI version information
subprocess.run(['wget', '-q', '-O', 'pypi_version.json', 'https://pypi.org/pypi/PyPtt/json'], check=True)
with open('pypi_version.json', 'r', encoding='utf-8') as f:
pypi_data = f.read()
pypi_version = pypi_data.split('"version":')[1].split('"')[1]
if pypi_version.startswith(main_version):
min_pypi_version = pypi_version.split('.')[-1]
# the next version
version = f"{main_version}.{int(min_pypi_version) + 1}"
else:
version = f"{main_version}.0"
break
except subprocess.CalledProcessError:
time.sleep(1)
if version is None or pypi_version is None:
raise ValueError('Can not get version from pypi')
if not is_merged:
commit_file = '/tmp/commit_hash.txt'
if os.path.exists(commit_file):
with open(commit_file, 'r', encoding='utf-8') as f:
commit_hash = f.read().strip()
else:
max_hash_length = 5
try:
commit_hash = subprocess.check_output(['git', 'rev-parse', '--long', 'HEAD']).decode('utf-8').strip()
except subprocess.CalledProcessError:
commit_hash = '0' * max_hash_length
commit_hash = ''.join([x for x in list(commit_hash) if x.isdigit()])
if len(commit_hash) < max_hash_length:
commit_hash = commit_hash + '0' * (max_hash_length - len(commit_hash))
commit_hash = commit_hash[:max_hash_length]
with open(commit_file, 'w', encoding='utf-8') as f:
f.write(commit_hash)
version = f"{version}.dev{commit_hash}"
if '__version__' in data:
current_version = data.split('__version__ = ')[1].split('\n')[0].strip().strip('\'')
data = data.replace(f"__version__ = '{current_version}'", f"__version__ = '{version}'")
else:
data += f'\n\n__version__ = \'{version}\''
with open('PyPtt/__init__.py', 'w', encoding='utf-8') as f:
f.write(data)
f.write('\n')
return version
================================================
FILE: setup.py
================================================
import os
import subprocess
import time
from setuptools import setup
def version_automation_script():
is_merged = os.environ.get('GITHUB_EVENT_NAME') == 'pull_request'
print('is_merged:', is_merged)
# read the main version from __init__.py
with open('PyPtt/__init__.py', 'r', encoding='utf-8') as f:
data = f.read().strip()
main_version = data.split('_main_version = ')[1].split('\n')[0].strip().strip('\'')
print('main_version version:', main_version)
version = None
pypi_version = None
for i in range(5):
try:
# Use wget to retrieve the PyPI version information
subprocess.run(['wget', '-q', '-O', 'pypi_version.json', 'https://pypi.org/pypi/PyPtt/json'], check=True)
with open('pypi_version.json', 'r', encoding='utf-8') as f:
pypi_data = f.read()
pypi_version = pypi_data.split('"version":')[1].split('"')[1]
if pypi_version.startswith(main_version):
min_pypi_version = pypi_version.split('.')[-1]
# the next version
version = f"{main_version}.{int(min_pypi_version) + 1}"
else:
version = f"{main_version}.0"
break
except subprocess.CalledProcessError:
time.sleep(1)
if version is None or pypi_version is None:
raise ValueError('Can not get version from pypi')
if not is_merged:
commit_file = '/tmp/commit_hash.txt'
if os.path.exists(commit_file):
with open(commit_file, 'r', encoding='utf-8') as f:
commit_hash = f.read().strip()
else:
max_hash_length = 5
try:
commit_hash = subprocess.check_output(['git', 'rev-parse', '--long', 'HEAD']).decode('utf-8').strip()
except subprocess.CalledProcessError:
commit_hash = '0' * max_hash_length
commit_hash = ''.join([x for x in list(commit_hash) if x.isdigit()])
if len(commit_hash) < max_hash_length:
commit_hash = commit_hash + '0' * (max_hash_length - len(commit_hash))
commit_hash = commit_hash[:max_hash_length]
with open(commit_file, 'w', encoding='utf-8') as f:
f.write(commit_hash)
version = f"{version}.dev{commit_hash}"
if '__version__' in data:
current_version = data.split('__version__ = ')[1].split('\n')[0].strip().strip('\'')
data = data.replace(f"__version__ = '{current_version}'", f"__version__ = '{version}'")
else:
data += f'\n\n__version__ = \'{version}\''
with open('PyPtt/__init__.py', 'w', encoding='utf-8') as f:
f.write(data)
f.write('\n')
return version
version = version_automation_script()
print('the next version:', version)
setup(
name='PyPtt', # Required
version=version, # Required
description='PyPtt\ngithub: https://github.com/PyPtt/PyPtt', # Required
long_description=open('README.md', encoding="utf-8").read(), # Optional
long_description_content_type='text/markdown',
url='https://pyptt.cc/', # Optional
author='CodingMan', # Optional
author_email='pttcodingman@gmail.com', # Optional
# https://pypi.org/classifiers/
classifiers=[ # Optional
'Development Status :: 5 - Production/Stable',
'Operating System :: OS Independent',
'Intended Audience :: Developers',
'Topic :: Communications :: BBS',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Internet',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3 :: Only',
'Natural Language :: Chinese (Traditional)',
'Natural Language :: English',
],
keywords=['PTT', 'crawler', 'bot', 'library', 'websockets'], # Optional
python_requires='>=3.8',
packages=['PyPtt'],
install_requires=[
'progressbar2',
'websockets',
'uao',
'requests',
'AutoStrEnum',
'PyYAML',
],
package_data={
'PyPtt': ['lang/*.yaml'],
}
)
================================================
FILE: tests/change_pw.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from tests import util
def test(ptt_bot: PyPtt.API):
ptt_bot.change_pw(ptt_bot._ptt_pw)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/comment.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from tests import util
def test(ptt_bot):
if ptt_bot.host == PyPtt.HOST.PTT1:
test_list = [
# comment the newest post
('Test', None),
]
else:
test_list = [
# comment the newest post
('Test', None),
]
for board, post_id in test_list:
if post_id is None:
newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, board)
for i in range(100):
post_info = ptt_bot.get_post(board, index=newest_index - i)
# if the post is not deleted, save the post
if post_info[PyPtt.PostField.post_status] == PyPtt.PostStatus.EXISTS:
break
print('post_id', post_id)
elif isinstance(post_id, int):
post_info = ptt_bot.get_post(board, index=post_id, query=True)
elif isinstance(post_id, str):
post_info = ptt_bot.get_post(board, aid=post_id, query=True)
print(post_info)
# comment by index
ptt_bot.comment(
board=board,
comment_type=PyPtt.CommentType.ARROW,
content='comment by index',
index=post_info['index'],
)
# comment by aid
ptt_bot.comment(
board=board,
comment_type=PyPtt.CommentType.ARROW,
content='comment by aid',
aid=post_info['aid'],
)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
# assert (result[0] == result[1])
if __name__ == '__main__':
func()
================================================
FILE: tests/config.py
================================================
import os
#
PTT1_ID = os.environ['PTT1_ID']
PTT1_PW = os.environ['PTT1_PW']
PTT2_ID = os.environ['PTT2_ID']
PTT2_PW = os.environ['PTT2_PW']
================================================
FILE: tests/exceptions.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
if __name__ == '__main__':
try:
raise PyPtt.NoPermission('test')
except PyPtt.Error as e:
print(e.__class__.__name__)
================================================
FILE: tests/get_board_info.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from PyPtt import log
from tests import util
def test(ptt_bot: PyPtt.API):
test_board = [
'SYSOP',
]
for board in test_board:
result = ptt_bot.get_board_info(board)
log.logger.info('get board info result', result)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/get_board_list.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import json
import PyPtt
from tests import util
def test(ptt_bot: PyPtt.API):
board_list = ptt_bot.get_all_boards()
with open(f'tests/{ptt_bot.host}-board_list.json', 'w') as f:
json.dump(board_list, f, indent=4, ensure_ascii=False)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/get_bottom_post_list.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from tests import util
def test(ptt_bot: PyPtt.API):
result = ptt_bot.get_bottom_post_list('Test')
print(result)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/get_favourite_boards.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from tests import util
def test(ptt_bot: PyPtt.API):
result = ptt_bot.get_favourite_boards()
for r in result:
print(r)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/get_mail.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from PyPtt import log
from tests import util
def test(ptt_bot: PyPtt.API):
result = []
for _ in range(3):
mail_index = ptt_bot.get_newest_index(PyPtt.NewIndex.MAIL)
mail_info = ptt_bot.get_mail(mail_index)
log.logger.info('mail result', mail_info)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/get_newest_index.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from PyPtt import log
from tests import util
def test_board_index(ptt_bot: PyPtt.API):
if ptt_bot.host == PyPtt.HOST.PTT1:
test_list = [
('Python', PyPtt.SearchType.KEYWORD, '[公告]'),
('ALLPOST', PyPtt.SearchType.KEYWORD, '(Wanted)'),
('Wanted', PyPtt.SearchType.KEYWORD, '(本文已被刪除)'),
('ALLPOST', PyPtt.SearchType.KEYWORD, '(Gossiping)'),
('Gossiping', PyPtt.SearchType.KEYWORD, '普悠瑪'),
('book', PyPtt.SearchType.KEYWORD, 'AWS'),
]
else:
test_list = [
('PttSuggest', PyPtt.SearchType.KEYWORD, '[問題]'),
# ('PttSuggest', PyPtt.SearchType.COMMENT, '10'),
]
for board, search_type, search_condition in test_list:
for _ in range(3):
index = ptt_bot.get_newest_index(
PyPtt.NewIndex.BOARD,
board)
log.logger.info(f'{board} newest index', index)
index = ptt_bot.get_newest_index(
PyPtt.NewIndex.BOARD,
board=board,
search_type=search_type,
search_condition=search_condition)
log.logger.info(f'{board} newest index with search', index)
def test_mail_index(ptt_bot: PyPtt.API):
for _ in range(3):
index = ptt_bot.get_newest_index(
PyPtt.NewIndex.MAIL)
log.logger.info('mail newest index', index)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test_mail_index(ptt_bot)
test_mail_index(ptt_bot)
test_board_index(ptt_bot)
test_board_index(ptt_bot)
test_mail_index(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/get_post.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import json
import PyPtt
from PyPtt import log
from tests import util
def test_no_condition(ptt_bot: PyPtt.API):
result = []
if ptt_bot.host == PyPtt.HOST.PTT1:
test_post_list = [
('Python', 1),
# ('NotExitBoard', 1),
('Python', '1TJH_XY0'),
# ('Python', '1TJdL7L8'),
# # 文章格式錯誤
# ('Stock', '1TVnEivO'),
# # 文章格式錯誤
# ('movie', 457),
# ('Gossiping', '1UDnXefr'),
# ('joke', '1Tc6G9eQ'),
# # 135193
# ('Test', 575),
# # 待證文章
# ('Test', '1U3pLzi0'),
# # 古早文章
# ('LAW', 1),
# # 辦刪除文章
# ('Test', 347),
# # comment number parse error
# ('Ptt25sign', '1VppdKLW'),
]
else:
test_post_list = [
('WhoAmI', 1),
]
for board, index in test_post_list:
if isinstance(index, int):
post = ptt_bot.get_post(
board,
index=index)
ptt_bot.get_post(
board,
index=index,
query=True)
else:
post = ptt_bot.get_post(
board,
aid=index)
ptt_bot.get_post(
board,
aid=index,
query=True)
result.append(post)
# util.log.py.info('+==+' * 10)
# util.log.py.info(post[PyPtt.PostField.content])
return result
def get_post_with_condition(ptt_bot: PyPtt.API):
def show_condition(test_board, search_type, condition):
if search_type == PyPtt.SearchType.KEYWORD:
type_str = '關鍵字'
if search_type == PyPtt.SearchType.AUTHOR:
type_str = '作者'
if search_type == PyPtt.SearchType.COMMENT:
type_str = '推文數'
if search_type == PyPtt.SearchType.MARK:
type_str = '標記'
if search_type == PyPtt.SearchType.MONEY:
type_str = '稿酬'
log.logger.info(f'{test_board} 使用 {type_str} 搜尋 {condition}')
if ptt_bot.config.host == PyPtt.HOST.PTT1:
test_list = [
('Python', PyPtt.SearchType.KEYWORD, '[公告]'),
('ALLPOST', PyPtt.SearchType.KEYWORD, '(Wanted)'),
('Wanted', PyPtt.SearchType.KEYWORD, '(本文已被刪除)'),
('ALLPOST', PyPtt.SearchType.KEYWORD, '(Gossiping)'),
('Gossiping', PyPtt.SearchType.KEYWORD, '普悠瑪'),
]
else:
test_list = [
('PttSuggest', PyPtt.SearchType.KEYWORD, '[問題]'),
('PttSuggest', PyPtt.SearchType.COMMENT, '10'),
]
result = []
test_range = 1
query = False
for (board, search_type, condition) in test_list:
show_condition(board, search_type, condition)
index = ptt_bot.get_newest_index(
PyPtt.NewIndex.BOARD,
board,
search_type=search_type,
search_condition=condition)
util.logger.info(f'{board} 最新文章編號 {index}')
for i in range(test_range):
post = ptt_bot.get_post(
board,
index=index - i,
# PostIndex=611,
search_type=search_type,
search_condition=condition,
query=query)
# print(json.dumps(post, indent=4))
log.logger.info('列表日期', post.get('list_date'))
log.logger.info('作者', post.get('author'))
log.logger.info('標題', post.get('title'))
if post.get('post_status') == PyPtt.PostStatus.EXISTS:
pass
# if not query:
# util.log.py.info('內文', post.get('content'))
elif post.get('post_status') == PyPtt.PostStatus.DELETED_BY_AUTHOR:
log.logger.info('文章被作者刪除')
elif post.get('post_status') == PyPtt.PostStatus.DELETED_BY_MODERATOR:
log.logger.info('文章被版主刪除')
log.logger.info('=' * 50)
result.append(post)
return result
def test(ptt_bot: PyPtt.API):
result = test_no_condition(ptt_bot)
print(result)
log.logger.info(json.dumps(result, indent=4, ensure_ascii=False))
# result = get_post_with_condition(ptt_bot)
# util.log.py.info(json.dumps(result, ensure_ascii=False, indent=4))
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/get_time.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from PyPtt import log
from tests import util
def test(ptt_bot: PyPtt.API):
result = []
for _ in range(10):
result.append(ptt_bot.get_time())
# time.sleep(1)
log.logger.info('get time result', result)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/get_user.py
================================================
import json
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from tests import util
def test(ptt_bot: PyPtt.API):
test_users = [
'CodingMan',
]
for test_user in test_users:
user_info = ptt_bot.get_user(test_user)
print(json.dumps(user_info, indent=4, ensure_ascii=False))
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/give_p.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from tests import util
def test(ptt_bot: PyPtt.API):
ptt_bot.give_money('janice001', 10)
def func():
host_list = [
PyPtt.HOST.PTT1,
# PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/i18n.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from PyPtt import i18n
from PyPtt import log
def test():
PyPtt.i18n.init(PyPtt.Language.ENGLISH)
print(PyPtt.i18n.goodbye)
logger = log.init(PyPtt.LogLevel.INFO, 'test')
logger.info(
i18n.replace(i18n.welcome, 'test version'))
if __name__ == '__main__':
test()
================================================
FILE: tests/init.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
def test():
print('=== default ===')
PyPtt.API()
print('=== 中文顯示 ===')
PyPtt.API(language=PyPtt.Language.MANDARIN)
print('=== 英文顯示 ===')
PyPtt.API(language=PyPtt.Language.ENGLISH)
print('=== log DEBUG ===')
PyPtt.API(log_level=PyPtt.LogLevel.DEBUG)
print('=== log INFO ===')
PyPtt.API(log_level=PyPtt.LogLevel.INFO)
print('=== log SILENT===')
PyPtt.API(log_level=PyPtt.LogLevel.SILENT)
print('=== set host with PTT ===')
ptt_bot = PyPtt.API(host=PyPtt.HOST.PTT1)
print(f'host result {ptt_bot.host}')
print('=== set host with PTT2 ===')
ptt_bot = PyPtt.API(host=PyPtt.HOST.PTT2)
print(f'host result {ptt_bot.host}')
print('=== set host with PTT and TELNET ===')
try:
PyPtt.API(host=PyPtt.HOST.PTT1, connect_mode=PyPtt.ConnectMode.TELNET)
assert False
except ValueError:
print('通過')
print('=== set host with PTT2 and TELNET ===')
try:
PyPtt.API(host=PyPtt.HOST.PTT2, connect_mode=PyPtt.ConnectMode.TELNET)
assert False
except ValueError:
print('通過')
try:
print('=== 語言 99 ===')
PyPtt.API(language=99)
except TypeError:
print('通過')
except:
print('沒通過')
assert False
print('=== 語言放字串 ===')
try:
PyPtt.API(language='PyPtt.i18n.language.ENGLISH')
except TypeError:
print('通過')
except:
print('沒通過')
assert False
print('complete')
if __name__ == '__main__':
test()
# PyPtt.API()
================================================
FILE: tests/logger.py
================================================
import os
import sys
sys.path.append(os.getcwd())
from PyPtt import log
def func():
logger = log.init(log.INFO)
logger.info('1')
logger.info('1', '2')
logger.info('1', '2', '3')
logger.debug('debug 1')
logger.debug('1', '2')
logger.debug('1', '2', '3')
logger = log.init(log.DEBUG)
logger.info('234')
logger.info('1', '2')
logger.info('1', '2', '3')
logger.debug('debug 2')
logger.debug('1', '2')
logger.debug('1', '2', '3')
if __name__ == '__main__':
func()
================================================
FILE: tests/login_logout.py
================================================
import os
import sys
import time
sys.path.append(os.getcwd())
import PyPtt
from tests import util
def test(ptt_bot: PyPtt.API):
util.login(ptt_bot, kick=True)
ptt_bot.logout()
print('wait', end=' ')
max_wait_time = 5
for sec in range(max_wait_time):
print(max_wait_time - sec, end=' ')
time.sleep(1)
print()
util.login(ptt_bot, kick=False)
ptt_bot.logout()
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
test(ptt_bot)
print('login logout test ok')
if __name__ == '__main__':
func()
================================================
FILE: tests/performance.py
================================================
import os
import sys
import time
sys.path.append(os.getcwd())
import PyPtt
from tests import util
def test(ptt_bot):
test_time = 500
print(f'效能測試 get_time {test_time} 次')
start_time = time.time()
for _ in range(test_time):
ptt_time = ptt_bot.get_time()
assert ptt_time is not None
end_time = time.time()
print(
F'Performance Test get_time {end_time - start_time} s')
print('Performance Test finish')
def func():
ptt_bot_list = [
PyPtt.API()]
for ptt_bot in ptt_bot_list:
util.login(ptt_bot)
test(ptt_bot)
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/post.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import time
import PyPtt
from PyPtt import PostField
from tests import util
from PyPtt import log
def test(ptt_bot: PyPtt.API):
content = '''
此為 PyPtt 貼文測試內容,如有打擾請告知。
官方網站: https://pyptt.cc
測試標記
781d16268c9f25a39142a17ff063ac029b1466ca14cb34f5d88fe8aadfeee053
'''
temp = ''
for i in range(100):
content = f'{content}\n={i}='
temp = f'{temp}\n={i}='
check_ = [
'781d16268c9f25a39142a17ff063ac029b1466ca14cb34f5d88fe8aadfeee053',
temp
]
check_range = 3
for _ in range(check_range):
ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content=content, sign_file=0)
time.sleep(1)
newest_index = ptt_bot.get_newest_index(index_type=PyPtt.NewIndex.BOARD, board='Test')
# find post what we post
post_list = []
for i in range(10):
post = ptt_bot.get_post(board='Test', index=newest_index - i)
if post[PostField.post_status] != PyPtt.PostStatus.EXISTS:
print(f'Post {newest_index - i} not exists')
continue
post_author = post[PostField.author]
post_author = post_author.split(' ')[0]
if post_author != ptt_bot.ptt_id:
print(f'Post {newest_index - i} author not match', post_author)
continue
post_list.append(newest_index - i)
if len(post_list) == check_range:
break
comment_check = []
for index in post_list:
for i in range(5):
comment_check.append(f'={i}=')
ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.ARROW, content=f'={i}=', index=index)
comment_check = list(set(comment_check))
time.sleep(1)
for i, index in enumerate(post_list):
log.logger.info('test', i)
post = ptt_bot.get_post(board='Test', index=index)
if post[PostField.post_status] != PyPtt.PostStatus.EXISTS:
log.logger.info('fail')
print(f'Post {index} not exists')
break
post_author = post[PostField.author]
post_author = post_author.split(' ')[0]
if post_author != ptt_bot.ptt_id:
log.logger.info('fail')
print(f'Post {index} author not match', post_author)
break
check = True
for c in check_:
if c not in post[PostField.content]:
check = False
break
if not check:
log.logger.info('fail')
print(f'Post {index} content not match')
break
cur_comment_check = set()
for comment in post[PostField.comments]:
if comment[PyPtt.CommentField.content] in comment_check:
cur_comment_check.add(comment[PyPtt.CommentField.content])
else:
log.logger.info('comment', comment[PyPtt.CommentField.content])
if len(cur_comment_check) != len(comment_check):
log.logger.info('fail')
print(f'Post {index} comment not match')
break
log.logger.info('pass')
# for index in post_list:
# ptt_bot.del_post(board='Test', index=index)
util.del_all_post(ptt_bot)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
util.login(ptt_bot)
test(ptt_bot)
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/reply.py
================================================
import os
import sys
import time
sys.path.append(os.getcwd())
import PyPtt
from PyPtt import log
from PyPtt import PostField
from tests import util
current_id = None
def test(ptt_bot: PyPtt.API):
ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content='測試內文', sign_file=0)
time.sleep(1)
newest_index = ptt_bot.get_newest_index(index_type=PyPtt.NewIndex.BOARD, board='Test')
for i in range(5):
cur_post = ptt_bot.get_post(board='Test', index=newest_index - i)
if cur_post[PostField.post_status] != PyPtt.PostStatus.EXISTS:
continue
cur_author = cur_post[PostField.author]
cur_author = cur_author.split(' ')[0]
if cur_author.lower() != ptt_bot.ptt_id.lower():
continue
ptt_bot.reply_post(
reply_to=PyPtt.ReplyTo.BOARD, board='Test', index=newest_index - i, content='PyPtt 程式回覆測試')
break
newest_index += 1
time.sleep(1)
posts = []
# 在十篇範圍內找尋我們的文章
for i in range(10):
cur_post = ptt_bot.get_post(board='Test', index=newest_index - i)
if cur_post[PostField.post_status] != PyPtt.PostStatus.EXISTS:
continue
cur_author = cur_post[PostField.author]
cur_author = cur_author.split(' ')[0]
if cur_author.lower() != ptt_bot.ptt_id.lower():
continue
posts.append(cur_post[PostField.aid])
log.logger.info('test')
if len(posts) < 2:
log.logger.info('len(posts) < 2, fail')
return
check = [
'[測試] PyPtt 程式貼文測試',
'Re: [測試] PyPtt 程式貼文測試'
]
check_result = True
for aid in posts:
post = ptt_bot.get_post(board='Test', aid=aid)
if post[PostField.post_status] != PyPtt.PostStatus.EXISTS:
log.logger.info('post[PostField.post_status] != PyPtt.PostStatus.EXISTS, fail')
check_result = False
break
if post[PostField.title] not in check:
log.logger.info('post[PostField.title] not in check, fail')
check_result = False
break
check.remove(post[PostField.title])
if check_result:
log.logger.info('pass')
else:
log.logger.info('fail')
util.del_all_post(ptt_bot)
def func():
global current_id
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/search_user.py
================================================
import os
import sys
sys.path.append(os.getcwd())
import PyPtt
from PyPtt import log
from tests import util
def test(ptt_bot: PyPtt.API):
test_users = [
'Coding',
]
for test_user in test_users:
result = ptt_bot.search_user(test_user)
log.logger.info('result', result)
def func():
host_list = [
PyPtt.HOST.PTT1,
PyPtt.HOST.PTT2
]
for host in host_list:
ptt_bot = PyPtt.API(
host=host,
# log_level=PyPtt.LogLevel.DEBUG,
)
try:
util.login(ptt_bot)
test(ptt_bot)
finally:
ptt_bot.logout()
if __name__ == '__main__':
func()
================================================
FILE: tests/service.py
================================================
import os
import sys
import threading
sys.path.append(os.getcwd())
import PyPtt
from PyPtt import Service
from tests import config
def api_test(thread_id, service):
result = service.call('get_time')
print(f'thread id {thread_id}', 'get_time', result)
result = service.call('get_aid_from_url', {'url': 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html'})
print(f'thread id {thread_id}', 'get_aid_from_url', result)
result = service.call('get_newest_index', {'index_type': PyPtt.NewIndex.BOARD, 'board': 'Python'})
print(f'thread id {thread_id}', 'get_newest_index', result)
def test():
pyptt_init_config = {
# 'language': PyPtt.Language.ENGLISH,
}
service = Service(pyptt_init_config)
try:
service.call('login', {'ptt_id': config.PTT1_ID, 'ptt_pw': config.PTT1_PW})
pool = []
for i in range(10):
t = threading.Thread(target=api_test, args=(i, service))
t.start()
pool.append(t)
for t in pool:
t.join()
service.call('logout')
finally:
service.close()
if __name__ == '__main__':
test()
# pass
================================================
FILE: tests/util.py
================================================
import json
import PyPtt
from PyPtt import PostField
from PyPtt import log
from . import config
def log_to_file(msg: str):
with open('single_log.txt', 'a', encoding='utf8') as f:
f.write(f'{msg}\n')
def get_id_pw(password_file):
try:
with open(password_file) as AccountFile:
account = json.load(AccountFile)
ptt_id = account['id']
password = account['pw']
except FileNotFoundError:
print(f'Please write PTT ID and Password in {password_file}')
print('{"id":"your ptt id", "pw":"your ptt pw"}')
assert False
return ptt_id, password
def login(ptt_bot: PyPtt.API, kick: bool = True):
if ptt_bot.host == PyPtt.HOST.PTT1:
ptt_id, ptt_pw = config.PTT1_ID, config.PTT1_PW
else:
ptt_id, ptt_pw = config.PTT2_ID, config.PTT2_PW
for _ in range(3):
try:
ptt_bot.login(ptt_id=ptt_id, ptt_pw=ptt_pw, kick_other_session=kick)
break
except PyPtt.LoginError:
log.logger.info('登入失敗')
assert False
except PyPtt.WrongIDorPassword:
log.logger.info('帳號密碼錯誤')
assert False
except PyPtt.LoginTooOften:
log.logger.info('請稍等一下再登入')
assert False
if not ptt_bot.is_registered_user:
log.logger.info('未註冊使用者')
if ptt_bot.process_picks != 0:
log.logger.info(f'註冊單處理順位 {ptt_bot.process_picks}')
def show_data(data, key: str = None):
if isinstance(data, dict):
log.logger.info(f'{key}: {data[key]}')
def del_all_post(ptt_bot: PyPtt.API):
newest_index = ptt_bot.get_newest_index(index_type=PyPtt.NewIndex.BOARD, board='Test')
for i in range(30):
try:
ptt_bot.del_post(board='Test', index=newest_index - i)
except:
pass
================================================
FILE: upload.sh
================================================
#!/bin/sh
echo PyPtt uploader v 1.0.2
rm -r dist build
python3 setup.py sdist bdist_wheel --universal
case $1 in
release)
echo upload to pypi
python3 -m twine upload dist/*
;;
test)
echo upload to testpypi
python3 -m twine upload --repository testpypi dist/*
;;
*)
echo "unknown command [$@]"
;;
esac
echo Upload finish