Showing preview only (373K chars total). Download the full file or copy to clipboard to get everything.
Repository: PttCodingMan/PyPtt
Branch: master
Commit: f277f1adf595
Files: 119
Total size: 328.8 KB
Directory structure:
gitextract_7t96yf9r/
├── .github/
│ └── workflows/
│ ├── deploy.yml
│ ├── docs.yml
│ └── test.yml
├── .gitignore
├── GourceScript.bat
├── LICENSE
├── MANIFEST.in
├── PyPtt/
│ ├── PTT.py
│ ├── __init__.py
│ ├── _api_bucket.py
│ ├── _api_call_status.py
│ ├── _api_change_pw.py
│ ├── _api_comment.py
│ ├── _api_del_post.py
│ ├── _api_get_board_info.py
│ ├── _api_get_board_list.py
│ ├── _api_get_bottom_post_list.py
│ ├── _api_get_favourite_board.py
│ ├── _api_get_newest_index.py
│ ├── _api_get_post.py
│ ├── _api_get_post_index.py
│ ├── _api_get_time.py
│ ├── _api_get_user.py
│ ├── _api_give_money.py
│ ├── _api_has_new_mail.py
│ ├── _api_loginout.py
│ ├── _api_mail.py
│ ├── _api_mark_post.py
│ ├── _api_post.py
│ ├── _api_reply_post.py
│ ├── _api_search_user.py
│ ├── _api_set_board_title.py
│ ├── _api_util.py
│ ├── check_value.py
│ ├── command.py
│ ├── config.py
│ ├── connect_core.py
│ ├── data_type.py
│ ├── exceptions.py
│ ├── i18n.py
│ ├── lang/
│ │ ├── en_US.yaml
│ │ └── zh_TW.yaml
│ ├── lib_util.py
│ ├── log.py
│ ├── screens.py
│ └── service.py
├── README.md
├── docs/
│ ├── CNAME
│ ├── Makefile
│ ├── api/
│ │ ├── bucket.rst
│ │ ├── change_pw.rst
│ │ ├── comment.rst
│ │ ├── del_mail.rst
│ │ ├── del_post.rst
│ │ ├── get_aid_from_url.rst
│ │ ├── get_all_boards.rst
│ │ ├── get_board_info.rst
│ │ ├── get_bottom_post_list.rst
│ │ ├── get_favourite_boards.rst
│ │ ├── get_mail.rst
│ │ ├── get_newest_index.rst
│ │ ├── get_post.rst
│ │ ├── get_time.rst
│ │ ├── get_user.rst
│ │ ├── give_money.rst
│ │ ├── index.rst
│ │ ├── init.rst
│ │ ├── login_logout.rst
│ │ ├── mail.rst
│ │ ├── mark_post.rst
│ │ ├── post.rst
│ │ ├── reply_post.rst
│ │ ├── search_user.rst
│ │ └── set_board_title.rst
│ ├── changelog.rst
│ ├── conf.py
│ ├── dev.rst
│ ├── docker.rst
│ ├── examples.rst
│ ├── exceptions.rst
│ ├── faq.rst
│ ├── index.rst
│ ├── install.rst
│ ├── make.bat
│ ├── requirements.txt
│ ├── roadmap.rst
│ ├── robots.txt
│ ├── service.rst
│ └── type.rst
├── make_doc.sh
├── requirements.txt
├── scripts/
│ ├── lang.py
│ └── package_script.py
├── setup.py
├── tests/
│ ├── change_pw.py
│ ├── comment.py
│ ├── config.py
│ ├── exceptions.py
│ ├── get_board_info.py
│ ├── get_board_list.py
│ ├── get_bottom_post_list.py
│ ├── get_favourite_boards.py
│ ├── get_mail.py
│ ├── get_newest_index.py
│ ├── get_post.py
│ ├── get_time.py
│ ├── get_user.py
│ ├── give_p.py
│ ├── i18n.py
│ ├── init.py
│ ├── logger.py
│ ├── login_logout.py
│ ├── performance.py
│ ├── post.py
│ ├── reply.py
│ ├── search_user.py
│ ├── service.py
│ └── util.py
└── upload.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/deploy.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: deploy
# run on merge to master or manual trigger
on:
pull_request:
types: [ closed ]
branches:
- master
paths:
- 'PyPtt/*.py'
- 'setup.py'
workflow_dispatch:
jobs:
deploy:
name: Deploy to PyPI and Docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: |
python -m build
- name: Publish package to TestPyPI
if: github.event_name == 'workflow_dispatch' && github.event.pull_request.merged == false
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
- name: Publish package
if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
- name: Trigger Docker build
if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true
uses: InformaticsMatters/trigger-ci-action@1.0.1
with:
ci-owner: PyPtt
ci-repository: PyPtt_image
ci-user: PttCodingMan
ci-ref: refs/heads/main
ci-user-token: ${{ secrets.ACCESS_TOKEN }}
ci-name: build PyPtt image
================================================
FILE: .github/workflows/docs.yml
================================================
name: docs
# run on merge to master or manual trigger
on:
pull_request:
types: [ closed ]
paths:
- 'docs/**/*'
- 'PyPtt/*.py'
workflow_dispatch:
jobs:
build:
if: github.event_name != 'workflow_dispatch' && github.event.pull_request.merged == true
name: Build doc and Deploy
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
- uses: actions/checkout@v4
with:
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
- name: Build and Commit
uses: sphinx-notes/pages@v2
with:
requirements_path: docs/requirements.txt
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages
================================================
FILE: .github/workflows/test.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: test
# run on every PR creation, or manual trigger
# run on every push to non-master branch
on:
pull_request:
types:
- opened
push:
branches-ignore:
- 'master'
workflow_dispatch:
env:
DEP_PATH: requirements.txt
jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 PyPtt/ --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 PyPtt/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test
run: |
python tests/init.py
scan:
runs-on: ubuntu-latest
steps:
- name: Check out master
uses: actions/checkout@v4
- name: detect-secrets
uses: reviewdog/action-detect-secrets@master
with:
reporter: github-pr-review
- name: Security vulnerabilities scan
uses: aufdenpunkt/python-safety-check@master
with:
scan_requirements_file_only: true
================================================
FILE: .gitignore
================================================
__pycache__/
build/
dist/
/CrawlBoardResult.txt
/.pypirc
PTTLibrary.egg-info/
*Out.txt
/Big5Data.txt
PTTLibrary-*/
/PTTLibrary/i18n.txt
/Test.txt
Account*.txt
*.spec
/LogHandler.txt
.vscode
.idea/
venv/
/log.txt
PyPtt.egg-info/
/test_account*.txt
/test_result.txt
*.json
tests/ptt.sh
docs/_build/
.DS_Store
test*.py
ptt.sh
================================================
FILE: GourceScript.bat
================================================
@echo off
cls
gource --seconds-per-day 0.05 --title "PTT Library"
================================================
FILE: LICENSE
================================================
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
================================================
FILE: MANIFEST.in
================================================
include LICENSE
include README.md
# Include the data files
recursive-include PyPtt *
================================================
FILE: PyPtt/PTT.py
================================================
from __future__ import annotations
import functools
import threading
from typing import Dict, Tuple, Callable, List, Optional, Any
from . import __version__
from . import _api_bucket
from . import _api_change_pw
from . import _api_comment
from . import _api_del_post
from . import _api_get_board_info
from . import _api_get_board_list
from . import _api_get_bottom_post_list
from . import _api_get_favourite_board
from . import _api_get_newest_index
from . import _api_get_post
from . import _api_get_time
from . import _api_get_user
from . import _api_give_money
from . import _api_loginout
from . import _api_mail
from . import _api_mark_post
from . import _api_post
from . import _api_reply_post
from . import _api_search_user
from . import _api_set_board_title
from . import check_value
from . import config
from . import connect_core
from . import data_type
from . import i18n
from . import lib_util
from . import log
class API:
def __init__(self, **kwargs):
"""
初始化 PyPtt。
Args:
language (:ref:`language`): PyPtt 顯示訊息的語言。預設為 **MANDARIN**。
log_level (LogLevel_): PyPtt 顯示訊息的等級。預設為 **INFO**。
screen_timeout (int): 經過 screen_timeout 秒之後, PyPtt 將會判定無法判斷目前畫面的狀況。預設為 **3 秒**。
screen_long_timeout (int): 經過 screen_long_timeout 秒之後,PyPtt 將會判定無法判斷目前畫面的狀況,這會用在較長的等待時間,例如踢掉其他連線等等。預設為 **10 秒**。
screen_post_timeout (int): 經過 screen_post_timeout 秒之後,PyPtt 將會判定無法判斷目前畫面的狀況,這會用在較長的等待時間,例如發佈文章等等。預設為 **60 秒**。
connect_mode (:ref:`connect-mode`): PyPtt 連線的模式。預設為 **WEBSOCKETS**。
logger_callback (Callable): PyPtt 顯示訊息的 callback。預設為 None。
port (int): PyPtt 連線的 port。預設為 **23**。
host (:ref:`host`): PyPtt 連線的 PTT 伺服器。預設為 **PTT1**。
check_update (bool): 是否檢查 PyPtt 的更新。預設為 **True**。
Returns:
None
範例::
import PyPtt
ptt_bot = PyPtt.API()
參考: :ref:`language`、LogLevel_、:ref:`connect-mode`、:ref:`host`
.. _LogLevel: https://github.com/PttCodingMan/SingleLog/blob/d7c19a1b848dfb1c9df8201f13def9a31afd035c/SingleLog/SingleLog.py#L22
英文顯示範例::
import PyPtt
ptt_bot = PyPtt.API(
language=PyPtt.Language.ENGLISH)
除錯範例::
import PyPtt
ptt_bot = PyPtt.API(
log_level=PyPtt.LogLevel.DEBUG)
"""
log_level = kwargs.get('log_level', log.INFO)
if not isinstance(log_level, log.LogLv):
raise TypeError('[PyPtt] log_level must be log.Level')
logger_callback = kwargs.get('logger_callback', None)
log.init(log_level, logger_callback=logger_callback)
language = kwargs.get('language', data_type.Language.MANDARIN)
if not isinstance(language, str):
raise TypeError('[PyPtt] language must be PyPtt.Language')
if language not in i18n.locale_pool:
raise TypeError('[PyPtt] language must be PyPtt.Language')
self.config = config.Config()
self.config.log_level = log_level
self.config.language = language
print('language', self.config.language)
i18n.init(self.config.language)
self.is_mailbox_full: bool = False
self.is_registered_user: bool = False
self.process_picks: int = 0
self.ptt_id: str = ''
self._ptt_pw: str = ''
self._is_login: bool = False
host = kwargs.get('host', data_type.HOST.PTT1)
screen_timeout = kwargs.get('screen_timeout', 3.0)
screen_long_timeout = kwargs.get('screen_long_timeout', 10.0)
screen_post_timeout = kwargs.get('screen_post_timeout', 60.0)
check_value.check_type(host, (data_type.HOST, str), 'host')
check_value.check_type(screen_timeout, float, 'screen_timeout')
check_value.check_type(screen_long_timeout, float, 'screen_long_timeout')
check_value.check_type(screen_post_timeout, float, 'screen_post_timeout')
if screen_timeout != 0:
self.config.screen_timeout = screen_timeout
if screen_long_timeout != 0:
self.config.screen_long_timeout = screen_long_timeout
if screen_post_timeout != 0:
self.config.screen_post_timeout = screen_post_timeout
self.config.host = host
self.host = host
port = kwargs.get('port', 23)
check_value.check_type(port, int, 'port')
check_value.check_range(port, 1, 65535 - 1, 'port')
self.config.port = port
connect_mode = kwargs.get('connect_mode', data_type.ConnectMode.WEBSOCKETS)
check_value.check_type(connect_mode, data_type.ConnectMode, 'connect_mode')
if host in [data_type.HOST.PTT1, data_type.HOST.PTT2] and connect_mode is data_type.ConnectMode.TELNET:
raise ValueError('[PyPtt] TELNET is not available on PTT1 and PTT2')
self.config.connect_mode = connect_mode
self.connect_core = connect_core.API(self.config)
self._exist_board_list = []
self._moderators = dict()
self._thread_id = threading.get_ident()
self._goto_board_list = []
self._board_info_list = dict()
self._newest_index_data = data_type.TimedDict(timeout=2)
log.logger.debug('thread_id', self._thread_id)
log.logger.info(
i18n.replace(i18n.welcome, __version__))
log.logger.info('PyPtt', i18n.initialization)
if self.config.connect_mode == data_type.ConnectMode.TELNET:
log.logger.info(i18n.set_connect_mode, '...', i18n.connect_mode_TELNET)
elif self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:
log.logger.info(i18n.set_connect_mode, '...', i18n.connect_mode_WEBSOCKET)
if self.config.language == data_type.Language.MANDARIN:
log.logger.info(i18n.set_up_lang_module, '...', i18n.mandarin_module)
elif self.config.language == data_type.Language.ENGLISH:
log.logger.info(i18n.set_up_lang_module, '...', i18n.english_module)
if self.config.host == data_type.HOST.PTT1:
log.logger.info(i18n.set_connect_host, '...', i18n.PTT)
elif self.config.host == data_type.HOST.PTT2:
log.logger.info(i18n.set_connect_host, '...', i18n.PTT2)
elif self.config.host == data_type.HOST.LOCALHOST:
log.logger.info(i18n.set_connect_host, '...', i18n.localhost)
else:
log.logger.info(i18n.set_connect_host, '...', self.config.host)
log.logger.info('PyPtt', i18n.initialization, '...', i18n.done)
check_update = kwargs.get('check_update', True)
check_value.check_type(check_update, bool, 'check_update')
if check_update:
version_compare, remote_version = lib_util.sync_version()
if version_compare is data_type.Compare.SMALLER:
log.logger.info(i18n.current_version, __version__)
log.logger.info(i18n.new_version, remote_version)
elif version_compare is data_type.Compare.BIGGER:
log.logger.info(i18n.development_version, __version__)
else:
log.logger.info(i18n.latest_version, __version__)
else:
log.logger.info(i18n.current_version, __version__)
def __del__(self):
if log.logger:
log.logger.debug(i18n.goodbye)
def login(self, ptt_id: str, ptt_pw: str, kick_other_session: bool = False) -> None:
"""
登入 PTT。
Args:
ptt_id (str): PTT ID。
ptt_pw (str): PTT 密碼。
kick_other_session (bool): 是否踢掉其他登入的 session。預設為 False。
Returns:
None
Raises:
LoginError: 登入失敗。
WrongIDorPassword: 帳號或密碼錯誤。
OnlySecureConnection: 只能使用安全連線。
ResetYourContactEmail: 請先至信箱設定連絡信箱。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
ptt_bot.login(
ptt_id='ptt_id', ptt_pw='ptt_pw', kick_other_session=True)
except PyPtt.LoginError:
print('登入失敗')
except PyPtt.WrongIDorPassword:
print('帳號密碼錯誤')
except PyPtt.OnlySecureConnection:
print('只能使用安全連線')
except PyPtt.ResetYourContactEmail:
print('請先至信箱設定連絡信箱')
"""
_api_loginout.login(self, ptt_id, ptt_pw, kick_other_session)
def logout(self) -> None:
"""
登出 PTT。
Returns:
None
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_loginout.logout(self)
def get_time(self) -> str:
"""
取得 PTT 系統時間。
Returns:
None
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
time = ptt_bot.get_time()
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_get_time.get_time(self)
def get_post(self, board: str, aid: Optional[str] = None, index: Optional[int] = None,
search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None,
search_list: Optional[List[tuple]] = None, query: bool = False) -> Dict:
"""
取得文章。
Args:
board (str): 看板名稱。
aid (str): 文章編號。
index: 文章編號。
search_list (List[str]): 搜尋清單。
query (bool): 是否為查詢模式。
Returns:
Dict,文章內容。詳見 :ref:`post-field`
Raises:
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
使用 AID 範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
post_info = ptt_bot.get_post('Python', aid='1TJH_XY0')
# .. do something ..
finally:
ptt_bot.logout()
使用 index 範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
post_info = ptt_bot.get_post('Python', index=1)
# .. do something ..
finally:
ptt_bot.logout()
使用搜尋範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
post_info = ptt_bot.get_post(
'Python',
index=1,
search_list=[(PyPtt.SearchType.KEYWORD, 'PyPtt')]
)
# .. do something ..
finally:
ptt_bot.logout()
| 更多範例參考 :ref:`取得文章 <check_post_status>`
| 參考 :ref:`取得最新文章編號 <api-get-newest-index>`
"""
return _api_get_post.get_post(
self, board, aid=aid, index=index, search_type=search_type, search_condition=search_condition,
search_list=search_list, query=query)
def get_newest_index(self, index_type: data_type.NewIndex, board: Optional[str] = None,
search_type: Optional[data_type.SearchType] = None, search_condition: Optional[str] = None,
search_list: Optional[List[Tuple[Any | str]]] = None, ) -> int:
"""
取得最新文章或信箱編號。
Args:
index_type (:ref:`new-index`): 編號類型。
board (str): 看板名稱。
search_list (List[str]): 搜尋清單。
Returns:
int,最新文章或信箱編號。
Raises:
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
取得最新看板編號::
import PyPtt
ptt_bot = PyPtt.API()
# get newest index of board
try:
# .. login ..
newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, 'Python')
# .. do something ..
finally:
ptt_bot.logout()
取得最新文章編號使用搜尋::
import PyPtt
ptt_bot = PyPtt.API()
search_list = [(PyPtt.SearchType.KEYWORD, 'PyPtt')]
try:
# .. login ..
newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.BOARD, 'Python', search_list=search_list)
# .. do something ..
finally:
ptt_bot.logout()
取得最新信箱編號::
import PyPtt
ptt_bot = PyPtt.API()
# get newest index of mail
try:
# .. login ..
newest_index = ptt_bot.get_newest_index(PyPtt.NewIndex.MAIL)
# .. do something ..
finally:
ptt_bot.logout()
參考 :ref:`搜尋編號種類 <new-index>`、:ref:`取得文章 <api-get-post>`
"""
return _api_get_newest_index.get_newest_index(
self, index_type, board, search_type, search_condition, search_list)
def post(self, board: str, title_index: int, title: str, content: str, sign_file: [str | int] = 0) -> None:
"""
發文。
Args:
board (str): 看板名稱。
title_index (int): 文章標題編號。
title (str): 文章標題。
content (str): 文章內容。
sign_file (str | int): 編號或隨機簽名檔 (x),預設為 0 (不選)。
Returns:
None
Raises:
UnregisteredUser: 未註冊使用者。
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
NoPermission: 沒有發佈權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content='測試內容', sign_file=0)
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_post.post(self, board, title, content, title_index, sign_file)
def comment(self, board: str, comment_type: data_type.CommentType, content: str, aid: Optional[str] = None,
index: int = 0) -> None:
"""
推文。
Args:
board (str): 看板名稱。
comment_type (:ref:`comment-type`): 推文類型。
content (str): 推文內容。
aid (str): 文章編號。
index (int): 文章編號。
Returns:
None
Raises:
UnregisteredUser: 未註冊使用者。
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
NoSuchPost: 文章不存在。
NoPermission: 沒有推文權限。
NoFastComment: 推文間隔太短。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.PUSH, content='Comment by index', index=123)
ptt_bot.comment(board='Test', comment_type=PyPtt.CommentType.PUSH, content='Comment by index', aid='17MrayxF')
# .. do something ..
finally:
ptt_bot.logout()
參考 :ref:`推文類型 <comment-type>`、:ref:`取得最新文章編號 <api-get-newest-index>`
"""
_api_comment.comment(self, board, comment_type, content, aid, index)
def get_user(self, user_id: str) -> Dict:
"""
取得使用者資訊。
Args:
user_id (str): 使用者 ID。
Returns:
Dict,使用者資訊。詳見 :ref:`使用者資料欄位 <user-field>`
Raises:
RequireLogin: 需要登入。
NoSuchUser: 使用者不存在。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
user_info = ptt_bot.get_user('CodingMan')
# .. do something ..
finally:
ptt_bot.logout()
參考 :ref:`使用者資料欄位 <user-field>`
"""
return _api_get_user.get_user(self, user_id)
def give_money(self, ptt_id: str, money: int, red_bag_title: Optional[str] = None,
red_bag_content: Optional[str] = None) -> None:
"""
轉帳,詳見 `P 幣`_。
.. _`P 幣`: https://pttpedia.fandom.com/zh/wiki/P%E5%B9%A3
Args:
ptt_id (str): PTT ID。
money (int): 轉帳金額。
red_bag_title (str): 紅包標題。
red_bag_content (str): 紅包內容。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchUser: 使用者不存在。
NoMoney: 餘額不足。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.give_money(ptt_id='CodingMan', money=100)
# or
ptt_bot.give_money('CodingMan', 100, red_bag_title='紅包袋標題', red_bag_content='紅包袋內文')
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_give_money.give_money(self, ptt_id, money, red_bag_title, red_bag_content)
def mail(self, ptt_id: str, title: str, content: str, sign_file: [int | str] = 0,
backup: bool = True) -> None:
"""
寄信。
Args:
ptt_id (str): PTT ID。
title (str): 信件標題。
content (str): 信件內容。
sign_file (str | int): 編號或隨機簽名檔 (x),預設為 0 (不選)。
backup (bool): 如果是 True 寄信時將會備份信件,預設為 True。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchUser: 使用者不存在。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.mail(ptt_id='CodingMan', title='信件標題', content='信件內容')
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_mail.mail(self, ptt_id, title, content, sign_file, backup)
def get_all_boards(self) -> List[str]:
"""
取得全站看板清單。
Returns:
List[str],看板清單。
Raises:
RequireLogin: 需要登入。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
board_list = ptt_bot.get_all_boards()
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_get_board_list.get_board_list(self)
def reply_post(self, reply_to: data_type.ReplyTo, board: str, content: str, sign_file: [str | int] = 0,
aid: Optional[str] = None, index: int = 0) -> None:
"""
回覆文章。
Args:
reply_to (:ref:`reply-to`): 回覆類型。
board (str): 看板名稱。
content (str): 回覆內容。
sign_file (str | int): 編號或隨機簽名檔 (x),預設為 **0** (不選)。
aid: 文章編號。
index: 文章編號。
Returns:
None
Raises:
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
NoSuchPost: 文章不存在。
NoPermission: 沒有回覆權限。
CantResponse: 已結案並標記, 不得回應。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.reply_post(reply_to=PyPtt.ReplyTo.BOARD, board='Test', content='PyPtt 程式回覆測試', index=123)
# .. do something ..
finally:
ptt_bot.logout()
參考 :ref:`回覆類型 <reply-to>`、:ref:`取得最新文章編號 <api-get-newest-index>`
"""
_api_reply_post.reply_post(self, reply_to, board, content, sign_file, aid, index)
def set_board_title(self, board: str, new_title: str) -> None:
"""
設定看板標題。
Args:
board (str): 看板名稱。
new_title (str): 新標題。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchBoard: 看板不存在。
NeedModeratorPermission: 需要看板管理員權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.set_board_title(board='Test', new_title='現在時間 %s' % datetime.datetime.now())
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_set_board_title.set_board_title(self, board, new_title)
def mark_post(self, mark_type: int, board: str, aid: Optional[str] = None, index: int = 0, search_type: int = 0,
search_condition: Optional[str] = None) -> None:
"""
標記文章。
Args:
mark_type (:ref:`mark-type`): 標記類型。
board (str): 看板名稱。
aid (str): 文章編號。
index (int): 文章編號。
search_type (:ref:`search-type`): 搜尋類型。
search_condition (str): 搜尋條件。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchBoard: 看板不存在。
NoSuchPost: 文章不存在。
NeedModeratorPermission: 需要看板管理員權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.mark_post(mark_type=PyPtt.MarkType.M, board='Test', index=123)
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_mark_post.mark_post(self, mark_type, board, aid, index, search_type, search_condition)
def get_favourite_boards(self) -> List[dict]:
"""
取得我的最愛清單。
Returns:
List[dict],收藏看板清單,詳見 :ref:`favorite-board-field`。
Raises:
RequireLogin: 需要登入。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
favourite_boards = ptt_bot.get_favourite_boards()
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_get_favourite_board.get_favourite_board(self)
def bucket(self, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:
"""
水桶。
Args:
board (str): 看板名稱。
bucket_days (int): 水桶天數。
reason (str): 水桶原因。
ptt_id (str): PTT ID。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchBoard: 看板不存在。
NoSuchUser: 使用者不存在。
NeedModeratorPermission: 需要看板管理員權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.bucket(board='Test', bucket_days=7, reason='PyPtt 程式水桶測試', ptt_id='test')
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_bucket.bucket(self, board, bucket_days, reason, ptt_id)
def search_user(self, ptt_id: str, min_page: Optional[int] = None, max_page: Optional[int] = None) -> List[str]:
"""
搜尋使用者。
Args:
ptt_id (str): PTT ID。
min_page (int): 最小頁數。
max_page (int): 最大頁數。
Returns:
List[str],搜尋結果。
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
search_result = ptt_bot.search_user(ptt_id='Coding')
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_search_user.search_user(self, ptt_id, min_page, max_page)
def get_board_info(self, board: str, get_post_types: bool = False) -> Dict:
"""
取得看板資訊。
Args:
board (str): 看板名稱。
get_post_types (bool): 是否取得文章類型,例如:八卦板的「問卦」。
Returns:
Dict,看板資訊,詳見 :ref:`board-field`。
Raises:
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
NoPermission: 沒有權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
board_info = ptt_bot.get_board_info(board='Test')
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_get_board_info.get_board_info(self, board, get_post_types, call_by_others=False)
def get_mail(self, index: int, search_type: Optional[data_type.SearchType] = None,
search_condition: Optional[str] = None,
search_list: Optional[list] = None) -> Dict:
"""
取得信件。
Args:
index (int): 信件編號。
search_type (:ref:`search-type`): 搜尋類型。
search_condition: 搜尋條件。
search_list: 搜尋清單。
Returns:
Dict,信件資訊,詳見 :ref:`mail-field`。
Raises:
RequireLogin: 需要登入。
NoSuchMail: 信件不存在。
NoPermission: 沒有權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
mail = ptt_bot.get_mail(index=1)
# .. do something ..
finally:
ptt_bot.logout()
參考 :doc:`get_newest_index`
"""
return _api_mail.get_mail(self, index, search_type, search_condition, search_list)
def del_mail(self, index: int) -> None:
"""
刪除信件。
Args:
index (int): 信件編號。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
MailboxFull: 信箱已滿。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.del_mail(index=1)
# .. do something ..
finally:
ptt_bot.logout()
參考 :doc:`get_newest_index`
"""
_api_mail.del_mail(self, index)
def change_pw(self, new_password: str) -> None:
"""
更改密碼。
備註:因批踢踢系統限制,最長密碼為 8 碼。
Args:
new_password (str): 新密碼。
Returns:
None
Raises:
RequireLogin: 需要登入。
SetContactMailFirst: 需要先設定聯絡信箱。
WrongPassword: 密碼錯誤。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.change_pw(new_password='123456')
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_change_pw.change_pw(self, new_password)
@functools.lru_cache(maxsize=64)
def get_aid_from_url(self, url: str) -> Tuple[str, str]:
"""
從網址取得看板名稱與文章編號。
Args:
url: 網址。
Returns:
Tuple[str, str],看板名稱與文章編號。
範例::
import PyPtt
ptt_bot = PyPtt.API()
url = 'https://www.ptt.cc/bbs/Python/M.1565335521.A.880.html'
board, aid = ptt_bot.get_aid_from_url(url)
"""
return lib_util.get_aid_from_url(url)
def get_bottom_post_list(self, board: str) -> List[str]:
"""
取得看板置底文章清單。
Args:
board (str): 看板名稱。
Returns:
List[post],置底文章清單,詳見 :ref:`post-field`。
Raises:
RequireLogin: 需要登入。
NoSuchBoard: 看板不存在。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
bottom_post_list = ptt_bot.get_bottom_post_list(board='Python')
# .. do something ..
finally:
ptt_bot.logout()
"""
return _api_get_bottom_post_list.get_bottom_post_list(self, board)
def del_post(self, board: str, aid: Optional[str] = None, index: int = 0) -> None:
"""
刪除文章。
Args:
board (str): 看板名稱。
aid (str): 文章編號。
index (int): 文章編號。
Returns:
None
Raises:
RequireLogin: 需要登入。
UnregisteredUser: 未註冊使用者。
NoSuchBoard: 看板不存在。
NoSuchPost: 文章不存在。
NoPermission: 沒有權限。
範例::
import PyPtt
ptt_bot = PyPtt.API()
try:
# .. login ..
ptt_bot.del_post(board='Python', aid='1TJH_XY0')
# .. do something ..
finally:
ptt_bot.logout()
"""
_api_del_post.del_post(self, board, aid, index)
def fast_post_step0(self, board: str, title: str, content: str, post_type: int) -> None:
_api_post.fast_post_step0(self, board, title, content, post_type)
def fast_post_step1(self, sign_file):
_api_post.fast_post_step1(self, sign_file)
if __name__ == '__main__':
print('PyPtt v ' + __version__)
print('Maintained by CodingMan')
================================================
FILE: PyPtt/__init__.py
================================================
__version__ = '1.1.2'
from .PTT import API
from .data_type import *
from .exceptions import *
from .log import LogLevel
from .service import Service
LOG_LEVEL = LogLevel
_main_version = '1.2'
================================================
FILE: PyPtt/_api_bucket.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import screens
def bucket(api, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(board, str, 'board')
check_value.check_type(bucket_days, int, 'bucket_days')
check_value.check_type(reason, str, 'reason')
check_value.check_type(ptt_id, str, 'ptt_id')
api.get_user(ptt_id)
_api_util.check_board(api, board, check_moderator=True)
_api_util.goto_board(api, board)
cmd_list = []
cmd_list.append('i')
cmd_list.append(command.ctrl_p)
cmd_list.append('w')
cmd_list.append(command.enter)
cmd_list.append('a')
cmd_list.append(command.enter)
cmd_list.append(ptt_id)
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
cmd_list = []
cmd_list.append(str(bucket_days))
cmd_list.append(command.enter)
cmd_list.append(reason)
cmd_list.append(command.enter)
cmd_list.append('y')
cmd_list.append(command.enter)
cmd_part2 = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('◆ 使用者之前已被禁言', exceptions_=exceptions.UserHasPreviouslyBeenBanned()),
connect_core.TargetUnit('請以數字跟單位(預設為天)輸入期限', response=cmd_part2),
connect_core.TargetUnit('其它鍵結束', response=command.enter),
connect_core.TargetUnit('權限設定系統', response=command.enter),
connect_core.TargetUnit('任意鍵', response=command.space),
connect_core.TargetUnit(screens.Target.InBoard, break_detect=True),
]
api.connect_core.send(
cmd,
target_list)
================================================
FILE: PyPtt/_api_call_status.py
================================================
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import log
from . import screens
def get_call_status(api) -> None:
# log.py = DefaultLogger('api', api.config.log_level)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('A')
cmd_list.append(command.right)
cmd_list.append(command.left)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('[呼叫器]打開', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('[呼叫器]拔掉', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('[呼叫器]防水', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('[呼叫器]好友', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('[呼叫器]關閉', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('★', log_level=log.DEBUG, response=cmd),
]
for i in range(2):
index = api.connect_core.send(cmd, target_list)
if index < 0:
if i == 0:
continue
raise exceptions.UnknownError('UnknownError')
if index == 0:
return data_type.call_status.ON
if index == 1:
return data_type.call_status.UNPLUG
if index == 2:
return data_type.call_status.WATERPROOF
if index == 3:
return data_type.call_status.FRIEND
if index == 4:
return data_type.call_status.OFF
ori_screen = api.connect_core.get_screen_queue()[-1]
raise exceptions.UnknownError(ori_screen)
def set_call_status(api, call_status) -> None:
# 打開 -> 拔掉 -> 防水 -> 好友 -> 關閉
current_call_status = api._get_call_status()
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append(command.ctrl_u)
cmd_list.append('p')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InUserList, break_detect=True)]
while current_call_status != call_status:
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
current_call_status = api._get_call_status()
================================================
FILE: PyPtt/_api_change_pw.py
================================================
from . import command, _api_util
from . import connect_core
from . import exceptions
from . import i18n
from . import log
def change_pw(api, new_password: str) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
log.logger.info(i18n.change_pw)
new_password = new_password[:8]
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('U')
cmd_list.append(command.enter)
cmd_list.append('I')
cmd_list.append(command.enter)
cmd_list.append('2')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('設定聯絡信箱後才能修改密碼', exceptions_=exceptions.SetContactMailFirst()),
connect_core.TargetUnit('您輸入的密碼不正確', exceptions_=exceptions.WrongPassword()),
connect_core.TargetUnit('請您確定(Y/N)?', response='Y' + command.enter),
connect_core.TargetUnit('檢查新密碼', response=new_password + command.enter, max_match=1),
connect_core.TargetUnit('設定新密碼', response=new_password + command.enter, max_match=1),
connect_core.TargetUnit('輸入原密碼', response=api._ptt_pw + command.enter, max_match=1),
connect_core.TargetUnit('設定個人資料與密碼', break_detect=True)
]
index = api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
if index < 0:
ori_screen = api.connect_core.get_screen_queue()[-1]
raise exceptions.UnknownError(ori_screen)
api._ptt_pw = new_password
log.logger.info(i18n.change_pw, '...', i18n.success)
================================================
FILE: PyPtt/_api_comment.py
================================================
import collections
import time
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
comment_option = [
None,
data_type.CommentType.PUSH,
data_type.CommentType.BOO,
data_type.CommentType.ARROW,
]
def _comment(api,
board: str,
push_type: data_type.CommentType,
push_content: str,
post_aid: str,
post_index: int) -> None:
_api_util.goto_board(api, board)
cmd_list = []
if post_aid is not None:
cmd_list.append(lib_util.check_aid(post_aid))
elif post_index != 0:
cmd_list.append(str(post_index))
else:
raise ValueError('post_aid and post_index cannot be None at the same time')
cmd_list.append(command.enter)
cmd_list.append(command.comment)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('您覺得這篇', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(f'→ {api.ptt_id}: ', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('加註方式', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit('禁止快速連續推文', log_level=log.INFO, break_detect=True,
exceptions_=exceptions.NoFastComment()),
connect_core.TargetUnit('禁止短時間內大量推文', log_level=log.INFO, break_detect=True,
exceptions_=exceptions.NoFastComment()),
connect_core.TargetUnit('使用者不可發言', log_level=log.INFO, break_detect=True,
exceptions_=exceptions.NoPermission(i18n.no_permission)),
connect_core.TargetUnit('◆ 抱歉, 禁止推薦', log_level=log.INFO, break_detect=True,
exceptions_=exceptions.CantComment()),
]
index = api.connect_core.send(
cmd,
target_list)
if index == -1:
raise exceptions.UnknownError('unknown error in comment')
log.logger.debug(i18n.has_comment_permission)
cmd_list = []
if index == 0 or index == 1:
push_option_line = api.connect_core.get_screen_queue()[-1]
push_option_line = push_option_line.split('\n')[-1]
log.logger.debug('comment option line', push_option_line)
available_push_type = collections.defaultdict(lambda: False)
first_available_push_type = None
if '值得推薦' in push_option_line:
available_push_type[data_type.CommentType.PUSH] = True
if first_available_push_type is None:
first_available_push_type = data_type.CommentType.PUSH
if '只加→註解' in push_option_line:
available_push_type[data_type.CommentType.ARROW] = True
if first_available_push_type is None:
first_available_push_type = data_type.CommentType.ARROW
if '給它噓聲' in push_option_line:
available_push_type[data_type.CommentType.BOO] = True
if first_available_push_type is None:
first_available_push_type = data_type.CommentType.BOO
log.logger.debug('available_push_type', available_push_type)
if available_push_type[push_type] is False:
if first_available_push_type:
push_type = first_available_push_type
if True in available_push_type.values():
cmd_list.append(str(comment_option.index(push_type)))
cmd_list.append(push_content)
cmd_list.append(command.enter)
cmd_list.append('y')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
]
api.connect_core.send(
cmd,
target_list)
def comment(api, board: str, push_type: data_type.CommentType, push_content: str, post_aid: str,
post_index: int) -> None:
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
check_value.check_type(board, str, 'board')
if not isinstance(push_type, data_type.CommentType):
raise TypeError(f'CommentType must be data_type.CommentType')
check_value.check_type(push_content, str, 'push_content')
if post_aid is not None:
check_value.check_type(post_aid, str, 'aid')
check_value.check_type(post_index, int, 'index')
if len(board) == 0:
raise ValueError(f'wrong parameter board: {board}')
if post_index != 0 and isinstance(post_aid, str):
raise ValueError('wrong parameter index and aid can\'t both input')
if post_index == 0 and post_aid is None:
raise ValueError('wrong parameter index or aid must input')
if post_index != 0:
newest_index = api.get_newest_index(
data_type.NewIndex.BOARD,
board=board)
check_value.check_index('index', post_index, newest_index)
_api_util.check_board(api, board)
board_info = api._board_info_list[board.lower()]
if board_info[data_type.BoardField.is_comment_record_ip]:
log.logger.debug(i18n.record_ip)
if board_info[data_type.BoardField.is_comment_aligned]:
log.logger.debug(i18n.push_aligned)
max_push_length = 32
else:
log.logger.debug(i18n.not_push_aligned)
max_push_length = 43 - len(api.ptt_id)
else:
log.logger.debug(i18n.not_record_ip)
if board_info[data_type.BoardField.is_comment_aligned]:
log.logger.debug(i18n.push_aligned)
max_push_length = 46
else:
log.logger.debug(i18n.not_push_aligned)
max_push_length = 58 - len(api.ptt_id)
push_content = push_content.strip()
push_list = []
while push_content:
index = 0
jump = 0
while len(push_content[:index].encode('big5uao', 'replace')) < max_push_length:
if index == len(push_content):
break
if push_content[index] == '\n':
jump = 1
break
index += 1
push_list.append(push_content[:index])
push_content = push_content[index + jump:]
push_list = filter(None, push_list)
for comment in push_list:
log.logger.info(i18n.comment)
for _ in range(2):
try:
_comment(api, board, push_type, comment, post_aid=post_aid, post_index=post_index)
break
except exceptions.NoFastComment:
# screens.show(api.config, api.connect_core.getScreenQueue())
log.logger.info(i18n.wait_for_no_fast_comment)
time.sleep(5.2)
log.logger.info(i18n.comment, '...', i18n.success)
================================================
FILE: PyPtt/_api_del_post.py
================================================
from __future__ import annotations
from typing import Optional
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0) -> None:
_api_util.one_thread(api)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
check_value.check_type(board, str, 'board')
if post_aid is not None:
check_value.check_type(post_aid, str, 'PostAID')
check_value.check_type(post_index, int, 'PostIndex')
if len(board) == 0:
raise ValueError(f'board error parameter: {board}')
if post_index != 0 and isinstance(post_aid, str):
raise ValueError('wrong parameter index and aid can\'t both input')
if post_index == 0 and post_aid is None:
raise ValueError('wrong parameter index or aid must input')
if post_index != 0:
newest_index = api.get_newest_index(
data_type.NewIndex.BOARD,
board=board)
check_value.check_index(
'PostIndex',
post_index,
newest_index)
log.logger.info(i18n.delete_post)
board_info = _api_util.check_board(api, board)
check_author = True
for moderator in board_info[data_type.BoardField.moderators]:
if api.ptt_id.lower() == moderator.lower():
check_author = False
break
log.logger.info(i18n.delete_post)
post_info = api.get_post(board, aid=post_aid, index=post_index, query=True)
if post_info[data_type.PostField.post_status] != data_type.PostStatus.EXISTS:
# delete success
log.logger.info(i18n.success)
return
if check_author:
if api.ptt_id.lower() != post_info[data_type.PostField.author].lower():
log.logger.info(i18n.delete_post, '...', i18n.fail)
raise exceptions.NoPermission(i18n.no_permission)
_api_util.goto_board(api, board)
cmd_list = []
if post_aid is not None:
cmd_list.append(lib_util.check_aid(post_aid))
elif post_index != 0:
cmd_list.append(str(post_index))
else:
raise ValueError('post_aid and post_index cannot be None at the same time')
cmd_list.append(command.enter)
cmd_list.append('d')
cmd = ''.join(cmd_list)
api.confirm = False
def confirm_delete_handler(screen):
api.confirm = True
target_list = [
connect_core.TargetUnit('請按任意鍵繼續', response=' '),
connect_core.TargetUnit('請確定刪除(Y/N)?[N]', response='y' + command.enter, handler=confirm_delete_handler,
max_match=1),
connect_core.TargetUnit(screens.Target.InBoard, break_detect=True),
]
index = api.connect_core.send(
cmd,
target_list)
if index == 1:
if not api.confirm:
log.logger.info(i18n.delete_post, '...', i18n.fail)
raise exceptions.NoPermission(i18n.no_permission)
log.logger.info(i18n.delete_post, '...', i18n.success)
================================================
FILE: PyPtt/_api_get_board_info.py
================================================
import re
from typing import Dict
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens
from .data_type import BoardField
def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -> Dict:
logger = log.init(log.DEBUG if call_by_others else log.INFO)
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
check_value.check_type(board, str, 'board')
logger.info(
i18n.replace(i18n.get_board_info, board))
_api_util.goto_board(api, board, refresh=True)
ori_screen = api.connect_core.get_screen_queue()[-1]
# print(ori_screen)
nuser = None
for line in ori_screen.split('\n'):
if '編號' not in line:
continue
if '日 期' not in line:
continue
if '人氣' not in line:
continue
nuser = line
break
if nuser is None:
raise exceptions.NoSuchBoard(api.config, board)
# print('------------------------')
# print('nuser', nuser)
# print('------------------------')
if '[靜]' in nuser:
online_user = 0
else:
if '編號' not in nuser or '人氣' not in nuser:
raise exceptions.NoSuchBoard(api.config, board)
pattern = re.compile('[\d]+')
r = pattern.search(nuser)
if r is None:
raise exceptions.NoSuchBoard(api.config, board)
# 減一是把自己本身拿掉
online_user = int(r.group(0)) - 1
logger.debug('人氣', online_user)
target_list = [
connect_core.TargetUnit('任意鍵繼續', log_level=log.DEBUG if call_by_others else log.INFO,
break_detect=True),
]
api.connect_core.send(
'i',
target_list)
ori_screen = api.connect_core.get_screen_queue()[-1]
# print(ori_screen)
p = re.compile('《(.+)》看板設定')
r = p.search(ori_screen)
if r is not None:
boardname = r.group(0)[1:-5].strip()
logger.debug('看板名稱', boardname, board)
if boardname.lower() != board.lower():
raise exceptions.NoSuchBoard(api.config, board)
p = re.compile('中文敘述: (.+)')
r = p.search(ori_screen)
if r is not None:
chinese_des = r.group(0)[5:].strip()
logger.debug('中文敘述', chinese_des)
p = re.compile('板主名單: (.+)')
r = p.search(ori_screen)
if r is not None:
moderator_line = r.group(0)[5:].strip()
if '(無)' in moderator_line:
moderators = []
else:
moderators = moderator_line.split('/')
for moderator in moderators.copy():
if moderator == '徵求中':
moderators.remove(moderator)
logger.debug('板主名單', moderators)
open_status = ('公開狀態(是否隱形): 公開' in ori_screen)
logger.debug('公開狀態', open_status)
into_top_ten_when_hide = ('隱板時 可以 進入十大排行榜' in ori_screen)
logger.debug('隱板時可以進入十大排行榜', into_top_ten_when_hide)
non_board_members_post = ('開放 非看板會員發文' in ori_screen)
logger.debug('非看板會員發文', non_board_members_post)
reply_post = ('開放 回應文章' in ori_screen)
logger.debug('回應文章', reply_post)
self_del_post = ('開放 自刪文章' in ori_screen)
logger.debug('自刪文章', self_del_post)
push_post = ('開放 推薦文章' in ori_screen)
logger.debug('推薦文章', push_post)
boo_post = ('開放 噓文' in ori_screen)
logger.debug('噓文', boo_post)
# 限制 快速連推文章, 最低間隔時間: 5 秒
# 開放 快速連推文章
fast_push = ('開放 快速連推文章' in ori_screen)
logger.debug('快速連推文章', fast_push)
if not fast_push:
p = re.compile('最低間隔時間: [\d]+')
r = p.search(ori_screen)
if r is not None:
min_interval = r.group(0)[7:].strip()
min_interval = int(min_interval)
else:
min_interval = 0
logger.debug('最低間隔時間', min_interval)
else:
min_interval = 0
# 推文時 自動 記錄來源 IP
# 推文時 不會 記錄來源 IP
push_record_ip = ('推文時 自動 記錄來源 IP' in ori_screen)
logger.debug('記錄來源 IP', push_record_ip)
# 推文時 對齊 開頭
# 推文時 不用對齊 開頭
push_aligned = ('推文時 對齊 開頭' in ori_screen)
logger.debug('對齊開頭', push_aligned)
# 板主 可 刪除部份違規文字
moderator_can_del_illegal_content = ('板主 可 刪除部份違規文字' in ori_screen)
logger.debug('板主可刪除部份違規文字', moderator_can_del_illegal_content)
# 轉錄文章 會 自動記錄,且 需要 發文權限
tran_post_auto_recorded_and_require_post_permissions = ('轉錄文章 會 自動記錄,且 需要 發文權限' in ori_screen)
logger.debug('轉錄文章 會 自動記錄,且 需要 發文權限', tran_post_auto_recorded_and_require_post_permissions)
cool_mode = ('未 設為冷靜模式' not in ori_screen)
logger.debug('冷靜模式', cool_mode)
require18 = ('禁止 未滿十八歲進入' in ori_screen)
logger.debug('禁止未滿十八歲進入', require18)
p = re.compile('登入次數 [\d]+ 次以上')
r = p.search(ori_screen)
if r is not None:
require_login_time = r.group(0).split(' ')[1]
require_login_time = int(require_login_time)
else:
require_login_time = 0
logger.debug('發文限制登入次數', require_login_time)
p = re.compile('退文篇數 [\d]+ 篇以下')
r = p.search(ori_screen)
if r is not None:
require_illegal_post = r.group(0).split(' ')[1]
require_illegal_post = int(require_illegal_post)
else:
require_illegal_post = 0
logger.debug('發文限制退文篇數', require_illegal_post)
kind_list = []
if get_post_kind:
_api_util.goto_board(api, board)
# Go certain board, then post to get post type info
cmd_list = []
cmd_list.append(command.ctrl_p)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
connect_core.TargetUnit('或不選)', break_detect=True)
]
index = api.connect_core.send(
cmd,
target_list)
if index == 0:
raise exceptions.NoPermission(i18n.no_permission)
# no post permission
ori_screen = api.connect_core.get_screen_queue()[-1]
screen_lines = ori_screen.split('\n')
for i in screen_lines:
if '種類:' in i:
type_pattern = re.compile('\d\.([^\ ]*)')
# 0 is not present any type that the key hold None object
kind_list = type_pattern.findall(i)
break
# Clear post status
cmd_list = []
cmd_list.append(command.ctrl_c)
cmd_list.append(command.ctrl_c)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InBoard, break_detect=True)
]
api.connect_core.send(
cmd,
target_list)
logger.info(
i18n.replace(i18n.get_board_info, board),
'...', i18n.success
)
return {
BoardField.board: boardname,
BoardField.online_user: online_user,
BoardField.mandarin_des: chinese_des,
BoardField.moderators: moderators,
BoardField.open_status: open_status,
BoardField.into_top_ten_when_hide: into_top_ten_when_hide,
BoardField.can_non_board_members_post: non_board_members_post,
BoardField.can_reply_post: reply_post,
BoardField.self_del_post: self_del_post,
BoardField.can_comment_post: push_post,
BoardField.can_boo_post: boo_post,
BoardField.can_fast_push: fast_push,
BoardField.min_interval_between_comments: min_interval,
BoardField.is_comment_record_ip: push_record_ip,
BoardField.is_comment_aligned: push_aligned,
BoardField.can_moderators_del_illegal_content: moderator_can_del_illegal_content,
BoardField.does_tran_post_auto_recorded_and_require_post_permissions: tran_post_auto_recorded_and_require_post_permissions,
BoardField.is_cool_mode: cool_mode,
BoardField.is_require18: require18,
BoardField.require_login_time: require_login_time,
BoardField.require_illegal_post: require_illegal_post,
BoardField.post_kind_list: kind_list
}
================================================
FILE: PyPtt/_api_get_board_list.py
================================================
import progressbar
from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens
def get_board_list(api) -> list:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
cmd_list = [
command.go_main_menu,
'F',
command.enter,
'y',
'$']
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InBoardList, break_detect=True)
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
ori_screen = api.connect_core.get_screen_queue()[-1]
max_no = 0
for line in ori_screen.split('\n'):
if '◎' not in line and '●' not in line:
continue
if line.startswith(api.cursor):
line = line[len(api.cursor):]
# print(f'->{line}<')
if '◎' in line:
front_part = line[:line.find('◎')]
else:
front_part = line[:line.find('●')]
front_part_list = [x for x in front_part.split(' ')]
front_part_list = list(filter(None, front_part_list))
# print(f'FrontPartList =>{FrontPartList}<=')
max_no = int(front_part_list[0].rstrip(')'))
if api.config.log_level == log.INFO:
pb = progressbar.ProgressBar(
max_value=max_no,
redirect_stdout=True)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('F')
cmd_list.append(command.enter)
cmd_list.append('y')
cmd_list.append('0')
cmd = ''.join(cmd_list)
board_list = []
while True:
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
ori_screen = api.connect_core.get_screen_queue()[-1]
# print(OriScreen)
for line in ori_screen.split('\n'):
if '◎' not in line and '●' not in line:
continue
if line.startswith(api.cursor):
line = line[len(api.cursor):]
if '◎' in line:
front_part = line[:line.find('◎')]
else:
front_part = line[:line.find('●')]
front_part_list = [x for x in front_part.split(' ')]
front_part_list = list(filter(None, front_part_list))
number = front_part_list[0]
if ')' in number:
number = number[:number.rfind(')')]
no = int(number)
board_name = front_part_list[1]
if board_name.startswith('ˇ'):
board_name = board_name[1:]
if len(board_name) == 0:
board_name = front_part_list[2]
board_list.append(board_name)
if api.config.log_level == log.INFO:
pb.update(no)
if no >= max_no:
break
cmd = command.ctrl_f
if api.config.log_level == log.INFO:
pb.finish()
return board_list
================================================
FILE: PyPtt/_api_get_bottom_post_list.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import log
from . import screens
def get_bottom_post_list(api, board):
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
check_value.check_type(board, str, 'board')
log.logger.info(i18n.catch_bottom_post)
_api_util.check_board(api, board)
_api_util.goto_board(api, board, end=True)
last_screen = api.connect_core.get_screen_queue()[-1]
bottom_screen = [line for line in last_screen.split('\n') if '★' in line[:8]]
bottom_length = len(bottom_screen)
if bottom_length == 0:
log.logger.info(i18n.catch_bottom_post_success)
return list()
cmd_list = []
cmd_list.append(command.query_post)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(
screens.Target.PTT1_QueryPost if api.config.host == data_type.HOST.PTT1 else screens.Target.PTT2_QueryPost,
log_level=log.DEBUG, break_detect=True, refresh=False),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)),
]
aid_list = []
result = []
for _ in range(bottom_length):
api.connect_core.send(cmd, target_list)
last_screen = api.connect_core.get_screen_queue()[-1]
lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index = \
_api_util.parse_query_post(
api,
last_screen)
aid_list.append(post_aid)
cmd_list = []
cmd_list.append(command.enter)
cmd_list.append(command.up)
cmd_list.append(command.query_post)
cmd = ''.join(cmd_list)
aid_list.reverse()
for post_aid in aid_list:
current_post = api.get_post(board=board, aid=post_aid, query=True)
result.append(current_post)
log.logger.info(i18n.catch_bottom_post, '...', i18n.success)
return list(reversed(result))
================================================
FILE: PyPtt/_api_get_favourite_board.py
================================================
from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from .data_type import FavouriteBoardField
def get_favourite_board(api) -> list:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
cmd_list = [command.go_main_menu, 'F', command.enter, '0']
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('選擇看板', break_detect=True)
]
log.logger.info(i18n.get_favourite_board_list)
board_list = []
favourite_board_list = []
while True:
api.connect_core.send(
cmd,
target_list)
ori_screen = api.connect_core.get_screen_queue()[-1]
# print(OriScreen)
screen_buf = ori_screen
screen_buf = [x for x in screen_buf.split('\n')][3:-1]
# adjust for cursor
screen_buf[0] = ' ' + screen_buf[0][1:]
screen_buf = [x for x in screen_buf]
min_len = 47
for i, line in enumerate(screen_buf):
if len(screen_buf[i]) == 0:
continue
if len(screen_buf[i]) <= min_len:
# print(f'[{ScreenBuf[i]}]')
screen_buf[i] = screen_buf[i] + (' ' * ((min_len + 1) - len(screen_buf[i])))
screen_buf = [x[10:min_len - len(x)].strip() for x in screen_buf]
screen_buf = list(filter(None, screen_buf))
for i, line in enumerate(screen_buf):
if '------------' in line:
continue
temp = line.strip().split(' ')
no_space_temp = list(filter(None, temp))
board = no_space_temp[0]
if board.startswith('ˇ'):
board = board[1:]
board_type = no_space_temp[1]
title_start_index = temp.index(board_type) + 1
board_title = ' '.join(temp[title_start_index:])
# remove ◎
board_title = board_title[1:]
if board in board_list:
log.logger.info(i18n.success)
return favourite_board_list
board_list.append(board)
favourite_board_list.append({
FavouriteBoardField.board: board,
FavouriteBoardField.type: board_type,
FavouriteBoardField.title: board_title})
if len(screen_buf) < 20:
break
cmd = command.ctrl_f
log.logger.info(i18n.get_favourite_board_list, '...', i18n.success)
return favourite_board_list
================================================
FILE: PyPtt/_api_get_newest_index.py
================================================
from __future__ import annotations
import re
from typing import Optional
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type, lib_util
from . import exceptions
from . import i18n
from . import log
from . import screens
def _get_newest_index(api) -> int:
last_screen = api.connect_core.get_screen_queue()[-1]
last_screen_list = last_screen.split('\n')
last_screen_list = last_screen_list[3:]
last_screen_list = '\n'.join([x[:9] for x in last_screen_list])
# print(last_screen_list)
all_index = re.findall(r'\d+', last_screen_list)
if len(all_index) == 0:
return 0
all_index = list(map(int, all_index))
all_index.sort(reverse=True)
# print(all_index)
max_check_range = 6
newest_index = 0
for index_temp in all_index:
need_continue = True
if index_temp > max_check_range:
check_range = max_check_range
else:
check_range = index_temp
for i in range(1, check_range):
if str(index_temp - i) not in last_screen:
need_continue = False
break
if need_continue:
log.logger.debug(i18n.find_newest_index, index_temp)
newest_index = index_temp
break
if newest_index == 0:
raise exceptions.UnknownError('UnknownError')
return newest_index
def get_newest_index(api, index_type: data_type.NewIndex, board: Optional[str] = None,
search_type: data_type.SearchType = None, search_condition: Optional[str] = None,
search_list: Optional[list] = None) -> int:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if search_list is None:
search_list = []
else:
check_value.check_type(search_list, list, 'search_list')
if (search_type, search_condition) != (None, None):
search_list.insert(0, (search_type, search_condition))
for search_type, search_condition in search_list:
check_value.check_type(search_type, data_type.SearchType, 'search_type')
check_value.check_type(search_condition, str, 'search_condition')
check_value.check_type(index_type, data_type.NewIndex, 'index_type')
data_key = f'{index_type}_{board}_{search_list}'
if data_key in api._newest_index_data:
return api._newest_index_data[data_key]
if index_type == data_type.NewIndex.BOARD:
check_value.check_type(board, str, 'board')
_api_util.check_board(api, board)
_api_util.goto_board(api, board)
cmd_list = []
cmd_list.append('1')
cmd_list.append(command.enter)
cmd_list.append('$')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('沒有文章...', log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.MainMenu_Exiting,
exceptions_=exceptions.NoSuchBoard(api.config, board)),
]
index = api.connect_core.send(cmd, target_list)
if index < 0:
raise exceptions.NoSuchBoard(api.config, board)
if index == 0:
return 0
normal_newest_index = _get_newest_index(api)
if search_list is not None and len(search_list) > 0:
target_list.insert(2,
connect_core.TargetUnit(
screens.Target.InBoardWithCursor,
log_level=log.DEBUG,
break_detect=True))
cmd_list = _api_util.get_search_condition_cmd(index_type, search_list)
cmd_list.append('1')
cmd_list.append(command.enter)
cmd_list.append('$')
cmd = ''.join(cmd_list)
index = api.connect_core.send(cmd, target_list)
if index < 0:
raise exceptions.NoSuchBoard(api.config, board)
if index == 0:
return 0
newest_index = _get_newest_index(api)
if normal_newest_index == newest_index:
raise exceptions.NoSearchResult()
else:
newest_index = normal_newest_index
elif index_type == data_type.NewIndex.MAIL:
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
if board is not None:
raise ValueError('board should not input at NewIndex.MAIL.')
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
cmd_list.append('1')
cmd_list.append(command.enter)
cmd_list.append('$')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InMailBox, break_detect=True),
connect_core.TargetUnit(screens.Target.CursorToGoodbye, response=cmd),
]
def get_index(api):
current_capacity, _ = _api_util.get_mailbox_capacity(api)
last_screen = api.connect_core.get_screen_queue()[-1]
cursor_line = [x for x in last_screen.split('\n') if x.strip().startswith(api.cursor)][0]
list_index = int(re.compile('(\d+)').search(cursor_line).group(0))
if search_type == 0 and search_list is None:
if list_index > current_capacity:
newest_index = list_index
else:
newest_index = current_capacity
else:
newest_index = list_index
return newest_index
newest_index = 0
index = api.connect_core.send(
cmd,
target_list)
if index == 0:
normal_newest_index = get_index(api)
if search_list is not None and len(search_list) > 0:
target_list.insert(
2,
connect_core.TargetUnit(
screens.Target.InMailBoxWithCursor,
log_level=log.DEBUG,
break_detect=True)
)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
cmd_list.extend(
_api_util.get_search_condition_cmd(index_type, search_list)
)
cmd_list.append('1')
cmd_list.append(command.enter)
cmd_list.append('$')
cmd = ''.join(cmd_list)
index = api.connect_core.send(
cmd,
target_list)
if index in [0, 2]:
newest_index = get_index(api)
if normal_newest_index == newest_index:
raise exceptions.NoSearchResult()
else:
newest_index = normal_newest_index
api._newest_index_data[data_key] = newest_index
return newest_index
================================================
FILE: PyPtt/_api_get_post.py
================================================
from __future__ import annotations
import json
import re
import time
from typing import Dict, Optional
from AutoStrEnum import AutoJsonEncoder
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
from .data_type import PostField, CommentField
def get_post(api, board: str, aid: Optional[str] = None, index: Optional[int] = None,
search_list: Optional[list] = None,
search_type: Optional[data_type.SearchType] = None,
search_condition: Optional[str] = None, query: bool = False) -> Dict:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
check_value.check_type(board, str, 'board')
if aid is not None:
check_value.check_type(aid, str, 'aid')
if index is not None:
check_value.check_type(index, int, 'index')
if search_list is None:
search_list = []
else:
check_value.check_type(search_list, list, 'search_list')
if (search_type, search_condition) != (None, None):
search_list.insert(0, (search_type, search_condition))
for search_type, search_condition in search_list:
check_value.check_type(search_type, data_type.SearchType, 'search_type')
check_value.check_type(search_condition, str, 'search_condition')
if len(board) == 0:
raise ValueError(f'board error parameter: {board}')
if index is not None and isinstance(aid, str):
raise ValueError('wrong parameter index and aid can\'t both input')
if index is None and aid is None:
raise ValueError('wrong parameter index or aid must input')
search_cmd = None
if search_list is not None and len(search_list) > 0:
current_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board, search_list=search_list)
search_cmd = _api_util.get_search_condition_cmd(data_type.NewIndex.BOARD, search_list)
else:
current_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board)
if index is not None:
check_value.check_index('index', index, current_index)
max_retry = 2
post = {}
for i in range(max_retry):
try:
post = _get_post(api, board, aid, index, query, search_cmd)
if not post:
pass
elif not post[PostField.pass_format_check]:
pass
else:
break
except exceptions.UnknownError:
if i == max_retry - 1:
raise
except exceptions.NoSuchBoard:
if i == max_retry - 1:
raise
log.logger.debug('Wait for retry repost')
time.sleep(0.1)
post = json.dumps(post, cls=AutoJsonEncoder)
return json.loads(post)
def _get_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0, query: bool = False,
search_cmd_list: Optional[list[str]] = None) -> Dict:
_api_util.check_board(api, board)
_api_util.goto_board(api, board)
cmd_list = []
if post_aid is not None:
cmd_list.append(lib_util.check_aid(post_aid))
elif post_index != 0:
if search_cmd_list is not None:
cmd_list.extend(search_cmd_list)
cmd_list.append(str(max(1, post_index - 100)))
cmd_list.append(command.enter)
cmd_list.append(str(post_index))
else:
raise ValueError('post_aid and post_index cannot be None at the same time')
cmd_list.append(command.enter)
cmd_list.append(command.query_post)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(
screens.Target.PTT1_QueryPost if api.config.host == data_type.HOST.PTT1 else screens.Target.PTT2_QueryPost,
log_level=log.DEBUG, break_detect=True, refresh=False),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board)),
]
index = api.connect_core.send(cmd, target_list)
ori_screen = api.connect_core.get_screen_queue()[-1]
post = {
PostField.board: None,
PostField.aid: None,
PostField.index: None,
PostField.author: None,
PostField.date: None,
PostField.title: None,
PostField.content: None,
PostField.money: None,
PostField.url: None,
PostField.ip: None,
PostField.comments: [],
PostField.post_status: data_type.PostStatus.EXISTS,
PostField.list_date: None,
PostField.has_control_code: False,
PostField.pass_format_check: False,
PostField.location: None,
PostField.push_number: None,
PostField.is_lock: False,
PostField.full_content: None,
PostField.is_unconfirmed: False}
post_author = None
post_title = None
if index < 0 or index == 1:
# 文章被刪除
log.logger.debug(i18n.post_deleted)
log.logger.debug('OriScreen', ori_screen)
cursor_line = [line for line in ori_screen.split(
'\n') if line.startswith(api.cursor)]
if len(cursor_line) != 1:
raise exceptions.UnknownError(ori_screen)
cursor_line = cursor_line[0]
log.logger.debug('CursorLine', cursor_line)
pattern = re.compile('[\d]+\/[\d]+')
pattern_result = pattern.search(cursor_line)
if pattern_result is None:
list_date = None
else:
list_date = pattern_result.group(0)
list_date = list_date[-5:]
pattern = re.compile('\[[\w]+\]')
pattern_result = pattern.search(cursor_line)
if pattern_result is not None:
post_del_status = data_type.PostStatus.DELETED_BY_AUTHOR
else:
pattern = re.compile('<[\w]+>')
pattern_result = pattern.search(cursor_line)
post_del_status = data_type.PostStatus.DELETED_BY_MODERATOR
# > 79843 9/11 - □ (本文已被吃掉)<
# > 76060 8/28 - □ (本文已被刪除) [weida7332]
# print(f'O=>{CursorLine}<')
if pattern_result is not None:
post_author = pattern_result.group(0)[1:-1]
else:
post_author = None
post_del_status = data_type.PostStatus.DELETED_BY_UNKNOWN
log.logger.debug('ListDate', list_date)
log.logger.debug('PostAuthor', post_author)
log.logger.debug('post_del_status', post_del_status)
post.update({
PostField.board: board,
PostField.author: post_author,
PostField.list_date: list_date,
PostField.post_status: post_del_status,
PostField.pass_format_check: True
})
return post
elif index == 0:
lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index = \
_api_util.parse_query_post(
api,
ori_screen)
if lock_post:
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.list_date: list_date,
PostField.pass_format_check: True,
PostField.push_number: push_number,
PostField.is_lock: True})
return post
if query:
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.list_date: list_date,
PostField.pass_format_check: True,
PostField.push_number: push_number})
return post
origin_post, has_control_code = _api_util.get_content(api)
if origin_post is None:
log.logger.info(i18n.post_deleted)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.push_number: push_number,
PostField.is_unconfirmed: api.Unconfirmed
})
return post
post_author_pattern_new = re.compile('作者 (.+) 看板')
post_author_pattern_old = re.compile('作者 (.+)')
board_pattern = re.compile('看板 (.+)')
post_date = None
post_content = None
ip = None
location = None
push_list = []
# 格式確認,亂改的我也沒辦法Q_Q
origin_post_lines = origin_post.split('\n')
author_line = origin_post_lines[0]
if board.lower() == 'allpost':
board_line = author_line[author_line.find(')') + 1:]
pattern_result = board_pattern.search(board_line)
if pattern_result is not None:
board_temp = post_author = pattern_result.group(0)
board_temp = board_temp[2:].strip()
if len(board_temp) > 0:
board = board_temp
log.logger.debug(i18n.board, board)
pattern_result = post_author_pattern_new.search(author_line)
if pattern_result is not None:
post_author = pattern_result.group(0)
post_author = post_author[:post_author.rfind(')') + 1]
else:
pattern_result = post_author_pattern_old.search(author_line)
if pattern_result is None:
log.logger.info(i18n.substandard_post, i18n.author)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed, })
return post
post_author = pattern_result.group(0)
post_author = post_author[:post_author.rfind(')') + 1]
post_author = post_author[4:].strip()
log.logger.debug(i18n.author, post_author)
post_title_pattern = re.compile('標題 (.+)')
title_line = origin_post_lines[1]
pattern_result = post_title_pattern.search(title_line)
if pattern_result is None:
log.logger.info(i18n.substandard_post, i18n.title)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed, })
return post
post_title = pattern_result.group(0)
post_title = post_title[4:].strip()
log.logger.debug(i18n.title, post_title)
post_date_pattern = re.compile('時間 .{24}')
date_line = origin_post_lines[2]
pattern_result = post_date_pattern.search(date_line)
if pattern_result is None:
log.logger.info(i18n.substandard_post, i18n.date)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed, })
return post
post_date = pattern_result.group(0)
post_date = post_date[4:].strip()
log.logger.debug(i18n.date, post_date)
content_fail = True
if screens.Target.content_start not in origin_post:
# print('Type 1')
content_fail = True
else:
post_content = origin_post
post_content = post_content[
post_content.find(screens.Target.content_start) + len(screens.Target.content_start) + 1:]
# print('Type 2')
# print(f'PostContent [{PostContent}]')
for content_end in screens.Target.content_end_list:
# + 3 = 把 --\n 拿掉
# print(f'EC [{EC}]')
if content_end in post_content:
content_fail = False
post_content = post_content[:post_content.rfind(content_end) + 3]
origin_post_lines = origin_post[origin_post.find(content_end):]
# post_content = post_content.strip()
origin_post_lines = origin_post_lines.split('\n')
break
if content_fail:
log.logger.info(i18n.substandard_post, i18n.content)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed, })
return post
log.logger.debug(i18n.content, post_content)
info_lines = [line for line in origin_post_lines if line.startswith('※') or line.startswith('◆')]
pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+')
pattern_p2 = re.compile('[\d]+-[\d]+-[\d]+-[\d]+')
for line in reversed(info_lines):
log.logger.debug('IP Line', line)
# type 1
# ※ 編輯: CodingMan (111.243.146.98 臺灣)
# ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 111.243.146.98 (臺灣)
# type 2
# ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 116.241.32.178
# ※ 編輯: kill77845 (114.136.55.237), 12/08/2018 16:47:59
# type 3
# ※ 發信站: 批踢踢實業坊(ptt.cc)
# ◆ From: 211.20.78.69
# ※ 編輯: JCC 來自: 211.20.78.69 (06/20 10:22)
# ※ 編輯: JCC (118.163.28.150), 12/03/2015 14:25:35
pattern_result = pattern.search(line)
if pattern_result is not None:
ip = pattern_result.group(0)
location_temp = line[line.find(ip) + len(ip):].strip()
location_temp = location_temp.replace('(', '')
location_temp = location_temp[:location_temp.rfind(')')]
location_temp = location_temp.strip()
# print(f'=>[{LocationTemp}]')
if ' ' not in location_temp and len(location_temp) > 0:
location = location_temp
log.logger.debug('Location', location)
break
pattern_result = pattern_p2.search(line)
if pattern_result is not None:
ip = pattern_result.group(0)
ip = ip.replace('-', '.')
# print(f'IP -> [{IP}]')
break
if api.config.host == data_type.HOST.PTT1:
if ip is None:
log.logger.info(i18n.substandard_post, ip)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: False,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed, })
return post
log.logger.debug('IP', ip)
push_author_pattern = re.compile('[推|噓|→] [\w| ]+:')
push_date_pattern = re.compile('[\d]+/[\d]+ [\d]+:[\d]+')
push_ip_pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+')
push_list = []
for line in origin_post_lines:
if line.startswith('推'):
comment_type = data_type.CommentType.PUSH
elif line.startswith('噓 '):
comment_type = data_type.CommentType.BOO
elif line.startswith('→ '):
comment_type = data_type.CommentType.ARROW
else:
continue
result = push_author_pattern.search(line)
if result is None:
# 不符合推文格式
continue
push_author = result.group(0)[2:-1].strip()
log.logger.debug(i18n.comment_id, push_author)
result = push_date_pattern.search(line)
if result is None:
continue
push_date = result.group(0)
log.logger.debug(i18n.comment_date, push_date)
comment_ip = None
result = push_ip_pattern.search(line)
if result is not None:
comment_ip = result.group(0)
log.logger.debug(f'{i18n.comment} ip', comment_ip)
push_content = line[line.find(push_author) + len(push_author):]
# PushContent = PushContent.replace(PushDate, '')
if api.config.host == data_type.HOST.PTT1:
push_content = push_content[:push_content.rfind(push_date)]
else:
# → CodingMan:What is Ptt? 推 10/04 13:25
push_content = push_content[:push_content.rfind(push_date) - 2]
if comment_ip is not None:
push_content = push_content.replace(comment_ip, '')
push_content = push_content[push_content.find(':') + 1:].strip()
log.logger.debug(i18n.comment_content, push_content)
current_push = {
CommentField.type: comment_type,
CommentField.author: push_author,
CommentField.content: push_content,
CommentField.ip: comment_ip,
CommentField.time: push_date}
push_list.append(current_push)
post.update({
PostField.board: board,
PostField.aid: post_aid,
PostField.index: post_index,
PostField.author: post_author,
PostField.date: post_date,
PostField.title: post_title,
PostField.url: post_web,
PostField.money: post_money,
PostField.content: post_content,
PostField.ip: ip,
PostField.comments: push_list,
PostField.list_date: list_date,
PostField.has_control_code: has_control_code,
PostField.pass_format_check: True,
PostField.location: location,
PostField.push_number: push_number,
PostField.full_content: origin_post,
PostField.is_unconfirmed: api.Unconfirmed})
return post
================================================
FILE: PyPtt/_api_get_post_index.py
================================================
from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens
def get_post_index(api, board: str, aid: str) -> int:
_api_util.goto_board(api, board)
cmd_list = []
cmd_list.append('#')
cmd_list.append(aid)
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('找不到這個文章代碼', log_level=log.DEBUG,
exceptions_=exceptions.NoSuchPost(board, aid)),
# 此狀態下無法使用搜尋文章代碼(AID)功能
connect_core.TargetUnit('此狀態下無法使用搜尋文章代碼(AID)功能',
exceptions_=exceptions.CanNotUseSearchPostCode()),
connect_core.TargetUnit('沒有文章...', exceptions_=exceptions.NoSuchPost(board, aid)),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.InBoardWithCursor, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.MainMenu_Exiting, exceptions_=exceptions.NoSuchBoard(api.config, board))
]
index = api.connect_core.send(
cmd,
target_list
)
ori_screen = api.connect_core.get_screen_queue()[-1]
if index < 0:
# print(OriScreen)
raise exceptions.NoSuchBoard(api.config, board)
# if index == 5:
# print(OriScreen)
# raise exceptions.NoSuchBoard(api.config, board)
# print(index)
# print(OriScreen)
screen_list = ori_screen.split('\n')
line = [x for x in screen_list if x.startswith(api.cursor)]
line = line[0]
last_line = screen_list[screen_list.index(line) - 1]
# print(LastLine)
# print(line)
if '編號' in last_line and '人氣:' in last_line:
index = line[1:].strip()
index_fix = False
else:
index = last_line.strip()
index_fix = True
while ' ' in index:
index = index.replace(' ', ' ')
index_list = index.split(' ')
index = index_list[0]
if index == '★':
return 0
index = int(index)
if index_fix:
index += 1
# print(Index)
return index
================================================
FILE: PyPtt/_api_get_time.py
================================================
import re
from . import _api_util
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import log
from . import screens
pattern = re.compile('[\d]+:[\d][\d]')
def get_time(api) -> str:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('A')
cmd_list.append(command.right)
cmd_list.append(command.left)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
]
index = api.connect_core.send(cmd, target_list)
if index != 0:
return None
ori_screen = api.connect_core.get_screen_queue()[-1]
line_list = ori_screen.split('\n')[-3:]
# 0:00
for line in line_list:
if '星期' in line and '線上' in line and '我是' in line:
result = pattern.search(line)
if result is not None:
return result.group(0)
return None
================================================
FILE: PyPtt/_api_get_user.py
================================================
import json
import re
from typing import Dict
from AutoStrEnum import AutoJsonEncoder
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
from .data_type import UserField
def get_user(api, ptt_id: str) -> Dict:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(ptt_id, str, 'UserID')
if len(ptt_id) < 2:
raise ValueError(f'wrong parameter user_id: {ptt_id}')
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('T')
cmd_list.append(command.enter)
cmd_list.append('Q')
cmd_list.append(command.enter)
cmd_list.append(ptt_id)
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.AnyKey, break_detect=True),
connect_core.TargetUnit(screens.Target.InTalk, break_detect=True),
]
index = api.connect_core.send(
cmd,
target_list)
ori_screen = api.connect_core.get_screen_queue()[-1]
if index == 1:
raise exceptions.NoSuchUser(ptt_id)
# PTT1
# 《ID暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》小康 ($73866)
# 《登入次數》1118 次 (同天內只計一次) 《有效文章》15 篇 (退:0)
# 《目前動態》閱讀文章 《私人信箱》最近無新信件
# 《上次上站》10/06/2019 17:29:49 Sun 《上次故鄉》111.251.231.184
# 《 五子棋 》 0 勝 0 敗 0 和 《象棋戰績》 0 勝 0 敗 0 和
# https://github.com/Truth0906/PTTLibrary
# 強大的 PTT 函式庫
# 提供您 快速 穩定 完整 的 PTT API
# 提供專業的 PTT 機器人諮詢服務
# PTT2
# 《ID暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》家徒四壁 ($0)
# 《登入次數》8 次 (同天內只計一次) 《有效文章》0 篇
# 《目前動態》看板列表 《私人信箱》最近無新信件
# 《上次上站》10/06/2019 17:27:55 Sun 《上次故鄉》111.251.231.184
# 《 五子棋 》 0 勝 0 敗 0 和 《象棋戰績》 0 勝 0 敗 0 和
# 《個人名片》CodingMan 目前沒有名片
lines = ori_screen.split('\n')[1:]
def parse_user_info_from_line(line: str) -> (str, str):
part_0 = line[line.find('》') + 1:]
part_0 = part_0[:part_0.find('《')].strip()
part_1 = line[line.rfind('》') + 1:].strip()
return part_0, part_1
ptt_id, buff_1 = parse_user_info_from_line(lines[0])
money = int(int_list[0]) if len(int_list := re.findall(r'\d+', buff_1)) > 0 else buff_1
buff_0, buff_1 = parse_user_info_from_line(lines[1])
login_count = int(re.findall(r'\d+', buff_0)[0])
account_verified = ('同天內只計一次' in buff_0)
legal_post = int(re.findall(r'\d+', buff_1)[0])
# PTT2 沒有退文
if api.config.host == data_type.HOST.PTT1:
illegal_post = int(re.findall(r'\d+', buff_1)[1])
else:
illegal_post = None
activity, mail = parse_user_info_from_line(lines[2])
last_login_date, last_login_ip = parse_user_info_from_line(lines[3])
five_chess, chess = parse_user_info_from_line(lines[4])
signature_file = '\n'.join(lines[5:-1]).strip('\n')
log.logger.debug('ptt_id', ptt_id)
log.logger.debug('money', money)
log.logger.debug('login_count', login_count)
log.logger.debug('account_verified', account_verified)
log.logger.debug('legal_post', legal_post)
log.logger.debug('illegal_post', illegal_post)
log.logger.debug('activity', activity)
log.logger.debug('mail', mail)
log.logger.debug('last_login_date', last_login_date)
log.logger.debug('last_login_ip', last_login_ip)
log.logger.debug('five_chess', five_chess)
log.logger.debug('chess', chess)
log.logger.debug('signature_file', signature_file)
user = {
UserField.ptt_id: ptt_id,
UserField.money: money,
UserField.login_count: login_count,
UserField.account_verified: account_verified,
UserField.legal_post: legal_post,
UserField.illegal_post: illegal_post,
UserField.activity: activity,
UserField.mail: mail,
UserField.last_login_date: last_login_date,
UserField.last_login_ip: last_login_ip,
UserField.five_chess: five_chess,
UserField.chess: chess,
UserField.signature_file: signature_file,
}
user = json.dumps(user, cls=AutoJsonEncoder)
return json.loads(user)
================================================
FILE: PyPtt/_api_give_money.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import log
def give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag_content: str) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(ptt_id, str, 'ptt_id')
check_value.check_type(money, int, 'money')
if red_bag_title is not None:
check_value.check_type(red_bag_title, str, 'red_bag_title')
else:
red_bag_title = ''
if red_bag_content is not None:
check_value.check_type(red_bag_content, str, 'red_bag_content')
else:
red_bag_content = ''
log.logger.info(
i18n.replace(i18n.give_money_to, ptt_id, money))
# Check data_type.user
api.get_user(ptt_id)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('P')
cmd_list.append(command.enter)
cmd_list.append('P')
cmd_list.append(command.enter)
cmd_list.append('O')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
edit_red_bag_cmd_list = list()
edit_red_bag_target = connect_core.TargetUnit('要修改紅包袋嗎', response='n' + command.enter)
if red_bag_title != '' or red_bag_content != '':
edit_red_bag_cmd_list.append('y')
edit_red_bag_cmd_list.append(command.enter)
if red_bag_title != '':
edit_red_bag_cmd_list.append(command.down)
edit_red_bag_cmd_list.append(command.ctrl_y) # remove the red_bag_title
edit_red_bag_cmd_list.append(command.enter)
edit_red_bag_cmd_list.append(command.up)
edit_red_bag_cmd_list.append(f'標題: {red_bag_title}')
# reset cursor to original position
edit_red_bag_cmd_list.append(command.up * 2)
if red_bag_content != '':
edit_red_bag_cmd_list.append(command.down * 4)
edit_red_bag_cmd_list.append(command.ctrl_y * 8) # remove original red_bag_content
edit_red_bag_cmd_list.append(red_bag_content)
edit_red_bag_cmd_list.append(command.ctrl_x)
edit_red_bag_cmd = ''.join(edit_red_bag_cmd_list)
edit_red_bag_target = connect_core.TargetUnit('要修改紅包袋嗎', response=edit_red_bag_cmd)
target_list = [
connect_core.TargetUnit('你沒有那麼多Ptt幣喔!', break_detect=True, exceptions_=exceptions.NoMoney()),
connect_core.TargetUnit('金額過少,交易取消!', break_detect=True, exceptions_=exceptions.NoMoney()),
connect_core.TargetUnit('交易取消!', break_detect=True,
exceptions_=exceptions.UnknownError(i18n.transaction_cancelled)),
connect_core.TargetUnit('確定進行交易嗎?', response='y' + command.enter),
connect_core.TargetUnit('按任意鍵繼續', break_detect=True),
edit_red_bag_target,
connect_core.TargetUnit('要修改紅包袋嗎', response=command.enter),
connect_core.TargetUnit('完成交易前要重新確認您的身份', response=api._ptt_pw + command.enter),
connect_core.TargetUnit('他是你的小主人,是否匿名?', response='n' + command.enter),
connect_core.TargetUnit('要給他多少Ptt幣呢?', response=command.tab + str(money) + command.enter),
connect_core.TargetUnit('這位幸運兒的id', response=ptt_id + command.enter),
connect_core.TargetUnit('認證尚未過期', response='y' + command.enter),
connect_core.TargetUnit('交易正在進行中', response=command.space)
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout
)
log.logger.info(
i18n.replace(i18n.give_money_to, ptt_id, money),
'...', i18n.success)
================================================
FILE: PyPtt/_api_has_new_mail.py
================================================
import re
from . import _api_util
from . import command
from . import connect_core
from . import log
from . import screens
def has_new_mail(api) -> int:
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
# cmd_list.append('1')
# cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
current_capacity = None
plus_count = 0
index_pattern = re.compile('(\d+)')
checked_index_list = []
break_detect = False
target_list = [
connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True)
]
api.connect_core.send(
cmd,
target_list,
)
current_capacity, _ = _api_util.get_mailbox_capacity(api)
if current_capacity > 20:
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
cmd_list.append('1')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
while True:
if current_capacity > 20:
api.connect_core.send(
cmd,
target_list,
)
last_screen = api.connect_core.get_screen_queue()[-1]
last_screen_list = last_screen.split('\n')
last_screen_list = last_screen_list[3:-1]
last_screen_list = [x[:10] for x in last_screen_list]
current_plus_count = 0
for line in last_screen_list:
if str(current_capacity) in line:
break_detect = True
index_result = index_pattern.search(line)
if index_result is None:
continue
current_index = index_result.group(0)
if current_index in checked_index_list:
continue
checked_index_list.append(current_index)
if '+' not in line:
continue
current_plus_count += 1
plus_count += current_plus_count
if break_detect:
break
cmd = command.ctrl_f
return plus_count
================================================
FILE: PyPtt/_api_loginout.py
================================================
import re
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import log
from . import screens
def logout(api) -> None:
_api_util.one_thread(api)
log.logger.info(i18n.logout)
if not api._is_login:
log.logger.info(i18n.logout, '...', i18n.success)
return
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('g')
cmd_list.append(command.enter)
cmd_list.append('y')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('任意鍵', break_detect=True),
]
try:
api.connect_core.send(cmd, target_list)
api.connect_core.close()
except exceptions.ConnectionClosed:
pass
except RuntimeError:
pass
api._is_login = False
log.logger.info(i18n.logout, '...', i18n.success)
def login(api, ptt_id: str, ptt_pw: str, kick_other_session: bool):
_api_util.one_thread(api)
check_value.check_type(ptt_id, str, 'ptt_id')
check_value.check_type(ptt_pw, str, 'password')
check_value.check_type(kick_other_session, bool, 'kick_other_session')
if api._is_login:
api.logout()
api.config.kick_other_session = kick_other_session
def kick_other_login_display_msg():
if api.config.kick_other_session:
return i18n.kick_other_login
return i18n.not_kick_other_login
def kick_other_login_response(screen):
if api.config.kick_other_session:
return 'y' + command.enter
return 'n' + command.enter
api.is_mailbox_full = False
# def is_mailbox_full():
# log.log(
# api.config,
# LogLevel.INFO,
# i18n.MailBoxFull)
# api.is_mailbox_full = True
def register_processing(screen):
pattern = re.compile('[\d]+')
api.process_picks = int(pattern.search(screen).group(0))
if len(ptt_pw) > 8:
ptt_pw = ptt_pw[:8]
ptt_id = ptt_id.strip()
ptt_pw = ptt_pw.strip()
api.ptt_id = ptt_id
api._ptt_pw = ptt_pw
api.connect_core.connect()
log.logger.info(i18n.login_id, ptt_id)
target_list = [
connect_core.TargetUnit(screens.Target.InMailBox,
response=command.go_main_menu + 'A' + command.right + command.left, break_detect=True),
connect_core.TargetUnit(screens.Target.InMailMenu, response=command.go_main_menu),
connect_core.TargetUnit(screens.Target.MainMenu, break_detect=True),
connect_core.TargetUnit('【看板列表】', response=command.go_main_menu),
connect_core.TargetUnit('密碼不對', break_detect=True, exceptions_=exceptions.WrongIDorPassword()),
connect_core.TargetUnit('請重新輸入', break_detect=True, exceptions_=exceptions.WrongIDorPassword()),
connect_core.TargetUnit('登入太頻繁', response=' '),
connect_core.TargetUnit('系統過載', break_detect=True),
connect_core.TargetUnit('您要刪除以上錯誤嘗試的記錄嗎', response='y' + command.enter),
connect_core.TargetUnit('請選擇暫存檔 (0-9)[0]', response=command.enter),
connect_core.TargetUnit('有一篇文章尚未完成', response='Q' + command.enter),
connect_core.TargetUnit(
'請重新設定您的聯絡信箱', break_detect=True, exceptions_=exceptions.ResetYourContactEmail()),
# connect_core.TargetUnit(
# i18n.in_login_process_please_wait,
# '登入中,請稍候'),
connect_core.TargetUnit('密碼正確'),
# 密碼正確
connect_core.TargetUnit('您想刪除其他重複登入的連線嗎', response=kick_other_login_response),
connect_core.TargetUnit('◆ 您的註冊申請單尚在處理中', response=command.enter, handler=register_processing),
connect_core.TargetUnit('任意鍵', response=' '),
connect_core.TargetUnit('正在更新與同步線上使用者及好友名單'),
connect_core.TargetUnit('【分類看板】', response=command.go_main_menu),
connect_core.TargetUnit([
'大富翁',
'排行榜',
'名次',
'代號',
'暱稱',
'數目'
], response=command.go_main_menu),
connect_core.TargetUnit([
'熱門話題'
], response=command.go_main_menu),
connect_core.TargetUnit('您確定要填寫註冊單嗎', response=command.enter * 3),
connect_core.TargetUnit('以上資料是否正確', response='y' + command.enter),
connect_core.TargetUnit('另外若輸入後發生認證碼錯誤請先確認輸入是否為最後一封', response='x' + command.enter),
connect_core.TargetUnit('此帳號已設定為只能使用安全連線', exceptions_=exceptions.OnlySecureConnection())
]
# IAC = '\xff'
# WILL = '\xfb'
# NAWS = '\x1f'
cmd_list = []
# cmd_list.append(IAC + WILL + NAWS)
cmd_list.append(ptt_id + ',')
cmd_list.append(command.enter)
cmd_list.append(ptt_pw)
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
index = api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout,
refresh=False,
secret=True)
if index == 0:
current_capacity, max_capacity = _api_util.get_mailbox_capacity(api)
log.logger.info(i18n.has_new_mail_goto_main_menu)
if current_capacity > max_capacity:
api.is_mailbox_full = True
log.logger.info(i18n.mail_box_full)
if api.is_mailbox_full:
log.logger.info(i18n.use_mailbox_api_will_logout_after_execution)
target_list = [
connect_core.TargetUnit(screens.Target.MainMenu, break_detect=True)
]
cmd = command.go_main_menu + 'A' + command.right + command.left
index = api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout,
secret=True)
ori_screen = api.connect_core.get_screen_queue()[-1]
is_login = True
for t in screens.Target.MainMenu:
if t not in ori_screen:
is_login = False
break
if not is_login:
raise exceptions.LoginError()
if '> (' in ori_screen:
api.cursor = data_type.Cursor.NEW
log.logger.debug(i18n.new_cursor)
elif '●(' in ori_screen:
api.cursor = data_type.Cursor.OLD
log.logger.debug(i18n.old_cursor)
else:
raise exceptions.UnknownError()
screens.Target.InBoardWithCursor = screens.Target.InBoardWithCursor[:screens.Target.InBoardWithCursorLen]
screens.Target.InBoardWithCursor.append(api.cursor)
screens.Target.InMailBoxWithCursor = screens.Target.InMailBoxWithCursor[:screens.Target.InMailBoxWithCursorLen]
screens.Target.InMailBoxWithCursor.append(api.cursor)
screens.Target.CursorToGoodbye = screens.Target.CursorToGoodbye[:len(screens.Target.MainMenu)]
if api.cursor == '>':
screens.Target.CursorToGoodbye.append('> (G)oodbye')
else:
screens.Target.CursorToGoodbye.append('●(G)oodbye')
unregistered_user = True
if '(T)alk' in ori_screen:
unregistered_user = False
if '(P)lay' in ori_screen:
unregistered_user = False
if '(N)amelist' in ori_screen:
unregistered_user = False
if unregistered_user:
log.logger.info(i18n.unregistered_user_cant_use_all_api)
api.is_registered_user = not unregistered_user
if api.process_picks != 0:
log.logger.info(i18n.picks_in_register, api.process_picks)
api._is_login = True
log.logger.info(i18n.login_success)
================================================
FILE: PyPtt/_api_mail.py
================================================
from __future__ import annotations
import re
from typing import Dict, Optional
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
from .data_type import MailField
# 寄信
def mail(api,
ptt_id: str,
title: str,
content: str,
sign_file,
backup: bool = True) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(ptt_id, str, 'ptt_id')
check_value.check_type(title, str, 'title')
check_value.check_type(content, str, 'content')
api.get_user(ptt_id)
check_sign_file = False
for i in range(0, 10):
if str(i) == sign_file or i == sign_file:
check_sign_file = True
break
if not check_sign_file:
if sign_file.lower() != 'x':
raise ValueError(f'wrong parameter sign_file: {sign_file}')
cmd_list = []
# 回到主選單
cmd_list.append(command.go_main_menu)
# 私人信件區
cmd_list.append('M')
cmd_list.append(command.enter)
# 站內寄信
cmd_list.append('S')
cmd_list.append(command.enter)
# 輸入 id
cmd_list.append(ptt_id)
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
# 定義如何根據情況回覆訊息
target_list = [
connect_core.TargetUnit('主題:', break_detect=True),
connect_core.TargetUnit('【電子郵件】', exceptions_=exceptions.NoSuchUser(ptt_id))
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout
)
cmd_list = []
# 輸入標題
cmd_list.append(title)
cmd_list.append(command.enter)
# 輸入內容
cmd_list.append(content)
# 儲存檔案
cmd_list.append(command.ctrl_x)
cmd = ''.join(cmd_list)
# 定義如何根據情況回覆訊息
target_list = [
connect_core.TargetUnit('請按任意鍵繼續', response=command.enter, break_detect_after_send=True),
connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
connect_core.TargetUnit('是否自存底稿', response=('y' if backup else 'n') + command.enter),
connect_core.TargetUnit('選擇簽名檔', response=str(sign_file) + command.enter),
connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),
]
# 送出訊息
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_post_timeout)
log.logger.info(i18n.send_mail, i18n.success)
# --
# ※ 發信站: 批踢踢實業坊(ptt.cc)
# ◆ From: 220.142.14.95
content_start = '───────────────────────────────────────'
content_end = '--\n※ 發信站: 批踢踢實業坊(ptt.cc)'
content_ip_old = '◆ From: '
mail_author_pattern = re.compile('作者 (.+)')
mail_title_pattern = re.compile('標題 (.+)')
mail_date_pattern = re.compile('時間 (.+)')
ip_pattern = re.compile('[\d]+\.[\d]+\.[\d]+\.[\d]+')
def get_mail(api, index: int, search_type: Optional[data_type.SearchType] = None,
search_condition: Optional[str] = None,
search_list: Optional[list] = None) -> Dict:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
log.logger.info(i18n.get_mail)
if not isinstance(index, int):
raise ValueError('index must be int')
current_index = api.get_newest_index(data_type.NewIndex.MAIL)
if index <= 0 or current_index < index:
raise exceptions.NoSuchMail()
# check_value.check_index('index', index, current_index)
cmd_list = []
# 回到主選單
cmd_list.append(command.go_main_menu)
# 進入信箱
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
# 處理條件整理出指令
_cmd_list = _api_util.get_search_condition_cmd(data_type.NewIndex.MAIL, search_list)
cmd_list.extend(_cmd_list)
# 前進至目標信件位置
cmd_list.append(str(index))
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
# 有時候會沒有最底下一列,只好偵測游標是否出現
if api.cursor == data_type.Cursor.NEW:
space_length = 6 - len(api.cursor) - len(str(index))
else:
space_length = 5 - len(api.cursor) - len(str(index))
fast_target = f"{api.cursor}{' ' * space_length}{index}"
# 定義如何根據情況回覆訊息
target_list = [
connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(fast_target, log_level=log.DEBUG, break_detect=True)
]
# 送出訊息
api.connect_core.send(
cmd,
target_list)
# 取得信件全文
origin_mail, _ = _api_util.get_content(api, post_mode=False)
# 使用表示式分析信件作者
pattern_result = mail_author_pattern.search(origin_mail)
if pattern_result is None:
mail_author = None
else:
mail_author = pattern_result.group(0)[2:].strip()
# 使用表示式分析信件標題
pattern_result = mail_title_pattern.search(origin_mail)
if pattern_result is None:
mail_title = None
else:
mail_title = pattern_result.group(0)[2:].strip()
# 使用表示式分析信件日期
pattern_result = mail_date_pattern.search(origin_mail)
if pattern_result is None:
mail_date = None
else:
mail_date = pattern_result.group(0)[2:].strip()
# 從全文拿掉信件開頭作為信件內文
mail_content = origin_mail[origin_mail.find(content_start) + len(content_start) + 1:]
mail_content = mail_content[
mail_content.find(screens.Target.content_start) + len(screens.Target.content_start) + 1:]
for EC in screens.Target.content_end_list:
# + 3 = 把 --\n 拿掉
if EC in mail_content:
mail_content = mail_content[:mail_content.rfind(EC) + 3].rstrip()
break
# 紅包偵測
red_envelope = False
if content_end not in origin_mail and 'Ptt幣的大紅包喔' in origin_mail:
mail_content = mail_content.strip()
red_envelope = True
else:
mail_content = mail_content[:mail_content.rfind(content_end) + 3]
if red_envelope:
mail_ip = None
mail_location = None
else:
# ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 111.242.182.114
# ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 59.104.127.126 (臺灣)
# 非紅包開始解析 ip 與 地區
ip_line_list = origin_mail.split('\n')
ip_line = [x for x in ip_line_list if x.startswith(content_end[3:])]
if len(ip_line) == 0:
# 沒 ip 就沒地區
mail_ip = None
mail_location = None
else:
ip_line = ip_line[0]
result = ip_pattern.search(ip_line)
if result is None:
ip_line = [x for x in ip_line_list if x.startswith(content_ip_old)]
if len(ip_line) == 0:
mail_ip = None
else:
ip_line = ip_line[0]
result = ip_pattern.search(ip_line)
mail_ip = result.group(0)
else:
mail_ip = result.group(0)
location = ip_line[ip_line.find(mail_ip) + len(mail_ip):].strip()
if len(location) == 0:
mail_location = None
else:
# print(location)
mail_location = location[1:-1]
log.logger.info(i18n.get_mail, '...', i18n.success)
return {
MailField.origin_mail: origin_mail,
MailField.author: mail_author,
MailField.title: mail_title,
MailField.date: mail_date,
MailField.content: mail_content,
MailField.ip: mail_ip,
MailField.location: mail_location,
MailField.is_red_envelope: red_envelope}
def del_mail(api, index) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
current_index = api.get_newest_index(data_type.NewIndex.MAIL)
check_value.check_index(index, current_index)
cmd_list = []
# 進入主選單
cmd_list.append(command.go_main_menu)
# 進入信箱
cmd_list.append(command.ctrl_z)
cmd_list.append('m')
if index > 20:
# speed up
cmd_list.append(str(1))
cmd_list.append(command.enter)
# 前進到目標信件位置
cmd_list.append(str(index))
cmd_list.append(command.enter)
# 刪除
cmd_list.append('dy')
cmd_list.append(command.enter)
cmd = ''.join(cmd_list)
# 定義如何根據情況回覆訊息
target_list = [
connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True)
]
# 送出
api.connect_core.send(
cmd,
target_list)
if api.is_mailbox_full:
api.logout()
raise exceptions.MailboxFull()
================================================
FILE: PyPtt/_api_mark_post.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
from . import screens
def mark_post(api, mark_type: int, board: str, post_aid: str, post_index: int, search_type: int,
search_condition: str) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
if not isinstance(mark_type, data_type.MarkType):
raise TypeError(f'mark_type must be data_type.MarkType')
check_value.check_type(board, str, 'board')
if post_aid is not None:
check_value.check_type(post_aid, str, 'PostAID')
check_value.check_type(post_index, int, 'PostIndex')
if not isinstance(search_type, data_type.SearchType):
raise TypeError(f'search_type must be data_type.SearchType')
if search_condition is not None:
check_value.check_type(search_condition, str, 'SearchCondition')
if len(board) == 0:
raise ValueError(f'board error parameter: {board}')
if mark_type != data_type.MarkType.DELETE_D:
if post_index != 0 and isinstance(post_aid, str):
raise ValueError('wrong parameter index and aid can\'t both input')
if post_index == 0 and post_aid is None:
raise ValueError('wrong parameter index or aid must input')
if search_condition is not None and search_type == 0:
raise ValueError('wrong parameter index or aid must input')
if search_type == data_type.SearchType.COMMENT:
try:
S = int(search_condition)
except ValueError:
raise ValueError(f'wrong parameter search_condition: {search_condition}')
check_value.check_range(S, -100, 100, 'search_condition')
if post_aid is not None and search_condition is not None:
raise ValueError('wrong parameter aid and search_condition can\'t both input')
if post_index != 0:
newest_index = api.get_newest_index(
data_type.NewIndex.BOARD,
board=board,
search_type=search_type,
search_condition=search_condition)
check_value.check_index(
'index',
post_index,
max_value=newest_index)
if mark_type == data_type.MarkType.UNCONFIRMED:
# 批踢踢兔沒有待證文章功能 QQ
if api.config.host == data_type.HOST.PTT2:
raise exceptions.HostNotSupport(lib_util.get_current_func_name())
_api_util.check_board(
board,
check_moderator=True)
_api_util.goto_board(api, board)
cmd_list = []
if post_aid is not None:
cmd_list.append(lib_util.check_aid(post_aid))
cmd_list.append(command.enter)
elif post_index != 0:
if search_condition is not None:
if search_type == data_type.SearchType.KEYWORD:
cmd_list.append('/')
elif search_type == data_type.SearchType.AUTHOR:
cmd_list.append('a')
elif search_type == data_type.SearchType.COMMENT:
cmd_list.append('Z')
elif search_type == data_type.SearchType.MARK:
cmd_list.append('G')
elif search_type == data_type.SearchType.MONEY:
cmd_list.append('A')
cmd_list.append(search_condition)
cmd_list.append(command.enter)
cmd_list.append(str(post_index))
cmd_list.append(command.enter)
else:
raise ValueError('post_aid and post_index cannot be None at the same time')
if mark_type == data_type.MarkType.S:
cmd_list.append('L')
elif mark_type == data_type.MarkType.D:
cmd_list.append('t')
elif mark_type == data_type.MarkType.DELETE_D:
cmd_list.append(command.ctrl_d)
elif mark_type == data_type.MarkType.M:
cmd_list.append('m')
elif mark_type == data_type.MarkType.UNCONFIRMED:
cmd_list.append(command.ctrl_e + 'S')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('刪除所有標記', log_level=log.INFO, response='y' + command.enter),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.INFO, break_detect=True),
]
api.connect_core.send(cmd, target_list)
================================================
FILE: PyPtt/_api_post.py
================================================
from __future__ import annotations
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import log
def fast_post_step0(api, board: str, title: str, content: str, post_type: int) -> None:
_api_util.goto_board(api, board)
cmd_list = []
cmd_list.append(command.ctrl_p)
cmd_list.append(str(post_type))
cmd_list.append(command.enter)
cmd_list.append(str(title))
cmd_list.append(command.enter)
cmd_list.append(str(content))
cmd_list.append(command.ctrl_x)
cmd_list.append('s')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('發表文章於【', break_detect=True),
connect_core.TargetUnit('使用者不可發言', break_detect=True),
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
connect_core.TargetUnit('確定要儲存檔案嗎', break_detect=True)
]
index = api.connect_core.fast_send(cmd, target_list)
if index < 0:
raise exceptions.UnknownError('UnknownError')
if index == 1 or index == 2:
raise exceptions.NoPermission(i18n.no_permission)
def fast_post_step1(api: object, sign_file) -> None:
cmd = '\r'
target_list = [
connect_core.TargetUnit('發表文章於【', break_detect=True),
connect_core.TargetUnit('使用者不可發言', break_detect=True),
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
connect_core.TargetUnit('確定要儲存檔案嗎', break_detect=True),
connect_core.TargetUnit('x=隨機', response=str(sign_file) + '\r'),
]
index = api.connect_core.fast_send(cmd, target_list)
if index < 0:
raise exceptions.UnknownError('UnknownError')
def fast_post(
api: object,
board: str,
title: str,
content: str,
post_type: int,
sign_file) -> None:
_api_util.goto_board(api, board)
cmd_list = []
cmd_list.append(command.ctrl_p)
cmd_list.append(str(post_type))
cmd_list.append(command.enter)
cmd_list.append(str(title))
cmd_list.append(command.enter)
cmd_list.append(str(content))
cmd_list.append(command.ctrl_x)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('發表文章於【', break_detect=True),
connect_core.TargetUnit('使用者不可發言', break_detect=True),
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),
]
index = api.connect_core.fast_send(cmd, target_list)
if index < 0:
raise exceptions.UnknownError('UnknownError')
if index == 1 or index == 2:
raise exceptions.NoPermission(i18n.no_permission)
sign_file_list = [str(x) for x in range(0, 10)]
sign_file_list.append('x')
def post(api, board: str, title: str, content: str, title_index: int, sign_file: [str | int]) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(board, str, 'board')
check_value.check_type(title_index, int, 'title_index')
check_value.check_type(title, str, 'title')
check_value.check_type(content, str, 'content')
if str(sign_file).lower() not in sign_file_list:
raise ValueError(f'wrong parameter sign_file: {sign_file}')
_api_util.check_board(api, board)
_api_util.goto_board(api, board)
log.logger.info(i18n.post)
cmd_list = []
cmd_list.append(command.ctrl_p)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('發表文章於【', break_detect=True),
connect_core.TargetUnit('使用者不可發言', break_detect=True),
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
]
index = api.connect_core.send(cmd, target_list)
if index < 0:
raise exceptions.UnknownError('UnknownError')
if index == 1 or index == 2:
log.logger.info(i18n.post, '...', i18n.fail)
raise exceptions.NoPermission(i18n.no_permission)
log.logger.debug(i18n.has_post_permission)
content = lib_util.uniform_new_line(content)
cmd_list = []
cmd_list.append(str(title_index))
cmd_list.append(command.enter)
cmd_list.append(str(title))
cmd_list.append(command.enter)
cmd_list.append(command.ctrl_y * 40)
cmd_list.append(str(content))
cmd_list.append(command.ctrl_x)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_post_timeout)
log.logger.info(i18n.post, '...', i18n.success)
================================================
FILE: PyPtt/_api_reply_post.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import i18n
from . import lib_util
from . import log
def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: str, sign_file, post_aid: str,
post_index: int) -> None:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not isinstance(reply_to, data_type.ReplyTo):
raise TypeError(f'ReplyTo must be data_type.ReplyTo')
check_value.check_type(board, str, 'board')
check_value.check_type(content, str, 'content')
if post_aid is not None:
check_value.check_type(post_aid, str, 'PostAID')
if post_index != 0:
newest_index = api.get_newest_index(
data_type.NewIndex.BOARD,
board=board)
check_value.check_index(
'index',
post_index,
max_value=newest_index)
sign_file_list = ['x']
sign_file_list.extend([str(x) for x in range(0, 10)])
if str(sign_file).lower() not in sign_file_list:
raise ValueError(f'wrong parameter sign_file: {sign_file}')
if post_aid is not None and post_index != 0:
raise ValueError('wrong parameter aid and index can\'t both input')
_api_util.check_board(api, board)
_api_util.goto_board(api, board)
cmd_list = []
if post_aid is not None:
cmd_list.append(lib_util.check_aid(post_aid))
elif post_index != 0:
cmd_list.append(str(post_index))
else:
raise ValueError('post_aid and post_index cannot be None at the same time')
cmd_list.append(command.enter * 2)
cmd_list.append('r')
if reply_to == data_type.ReplyTo.BOARD:
log.logger.info(i18n.reply_board)
reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='F' + command.enter)
elif reply_to == data_type.ReplyTo.MAIL:
log.logger.info(i18n.reply_mail)
reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='M' + command.enter)
elif reply_to == data_type.ReplyTo.BOARD_MAIL:
log.logger.info(i18n.reply_board_mail)
reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='B' + command.enter)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
connect_core.TargetUnit('◆ 很抱歉, 此文章已結案並標記, 不得回應', log_level=log.INFO,
exceptions_=exceptions.CantResponse()),
connect_core.TargetUnit('(E)繼續編輯 (W)強制寫入', log_level=log.INFO, response='W' + command.enter),
connect_core.TargetUnit('請選擇簽名檔', response=str(sign_file) + command.enter),
connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter),
connect_core.TargetUnit('編輯文章', log_level=log.INFO,
response=str(content) + command.enter + command.ctrl_x),
connect_core.TargetUnit('請問要引用原文嗎', log_level=log.DEBUG, response='Y' + command.enter),
connect_core.TargetUnit('採用原標題[Y/n]?', log_level=log.DEBUG, response='Y' + command.enter),
reply_target_unit,
connect_core.TargetUnit('已順利寄出,是否自存底稿', log_level=log.DEBUG, response='Y' + command.enter),
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
log.logger.info(i18n.success)
================================================
FILE: PyPtt/_api_search_user.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
from . import log
def search_user(api, ptt_id: str, min_page: int, max_page: int) -> list:
_api_util.one_thread(api)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(ptt_id, str, 'ptt_id')
if min_page is not None:
check_value.check_index('min_page', min_page)
if max_page is not None:
check_value.check_index('max_page', max_page)
if min_page is not None and max_page is not None:
check_value.check_index_range('min_page', min_page, 'max_page', max_page)
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('T')
cmd_list.append(command.enter)
cmd_list.append('Q')
cmd_list.append(command.enter)
cmd_list.append(ptt_id)
cmd = ''.join(cmd_list)
if min_page is not None:
template = min_page
else:
template = 1
appendstr = ' ' * template
cmdtemp = cmd + appendstr
target_list = [
connect_core.TargetUnit('任意鍵', break_detect=True)]
resultlist = []
log.logger.info(i18n.search_user)
while True:
api.connect_core.send(
cmdtemp,
target_list)
ori_screen = api.connect_core.get_screen_queue()[-1]
# print(OriScreen)
# print(len(OriScreen.split('\n')))
if len(ori_screen.split('\n')) == 2:
result_id = ori_screen.split('\n')[1]
result_id = result_id[result_id.find(' ') + 1:].strip()
# print(result_id)
resultlist.append(result_id)
break
else:
ori_screen = ori_screen.split('\n')[3:-1]
ori_screen = '\n'.join(ori_screen)
templist = ori_screen.replace('\n', ' ')
while ' ' in templist:
templist = templist.replace(' ', ' ')
templist = templist.split(' ')
resultlist.extend(templist)
# print(templist)
# print(len(templist))
if len(templist) != 100 and len(templist) != 120:
break
template += 1
if max_page is not None:
if template > max_page:
break
cmdtemp = ' '
api.connect_core.send(
command.enter,
[
# 《ID暱稱》
connect_core.TargetUnit('《ID暱稱》', response=command.enter),
connect_core.TargetUnit('查詢網友', break_detect=True)
]
)
log.logger.info(i18n.success)
return list(filter(None, resultlist))
================================================
FILE: PyPtt/_api_set_board_title.py
================================================
from . import _api_util
from . import check_value
from . import command
from . import connect_core
from . import exceptions
from . import i18n
from . import lib_util
def set_board_title(api, board: str, new_title: str) -> None:
# 第一支板主專用 api
_api_util.one_thread(api)
_api_util.goto_board(api, board)
if not api._is_login:
raise exceptions.RequireLogin(i18n.require_login)
if not api.is_registered_user:
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
check_value.check_type(board, str, 'board')
check_value.check_type(new_title, str, 'new_title')
_api_util.check_board(
api,
board,
check_moderator=True)
cmd_list = []
cmd_list.append('I')
cmd_list.append(command.ctrl_p)
cmd_list.append('b')
cmd_list.append(command.enter)
cmd_list.append(command.backspace * 31)
cmd_list.append(new_title)
cmd_list.append(command.enter * 2)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('◆ 已儲存新設定', break_detect=True),
connect_core.TargetUnit('◆ 未改變任何設定', break_detect=True),
]
api.connect_core.send(
cmd,
target_list,
screen_timeout=api.config.screen_long_timeout)
================================================
FILE: PyPtt/_api_util.py
================================================
from __future__ import annotations
import functools
import re
import threading
from typing import Dict, Optional
from . import _api_get_board_info
from . import command
from . import connect_core
from . import data_type
from . import exceptions
from . import log
from . import screens
def get_content(api, post_mode: bool = True):
api.Unconfirmed = False
def is_unconfirmed_handler(screen):
api.Unconfirmed = True
if post_mode:
cmd = command.enter * 2
else:
cmd = command.enter
target_list = [
# 待證實文章
connect_core.TargetUnit('本篇文章內容經站方授權之板務管理人員判斷有尚待證實之處', response=' ',
handler=is_unconfirmed_handler),
connect_core.TargetUnit(screens.Target.PostEnd, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.InPost, log_level=log.DEBUG, break_detect=True),
connect_core.TargetUnit(screens.Target.PostNoContent, log_level=log.DEBUG, break_detect=True),
# 動畫文章
connect_core.TargetUnit(screens.Target.Animation, response=command.go_main_menu_type_q,
break_detect_after_send=True),
]
line_from_pattern = re.compile('[\d]+~[\d]+')
has_control_code = False
control_code_mode = False
push_start = False
content_start_exist = False
content_start_jump = False
content_start_jump_set = False
first_page = True
origin_post = []
stop_dict = dict()
while True:
index = api.connect_core.send(cmd, target_list)
if index == 3 or index == 4:
return None, False
last_screen = api.connect_core.get_screen_queue()[-1]
lines = last_screen.split('\n')
last_line = lines[-1]
lines.pop()
last_screen = '\n'.join(lines)
if screens.Target.content_start in last_screen and not content_start_exist:
content_start_exist = True
if content_start_exist:
if not content_start_jump_set:
if screens.Target.content_start not in last_screen:
content_start_jump = True
content_start_jump_set = True
else:
content_start_jump = False
pattern_result = line_from_pattern.search(last_line)
if pattern_result is None:
control_code_mode = True
has_control_code = True
else:
last_read_line_list = pattern_result.group(0).split('~')
last_read_line_a_temp = int(last_read_line_list[0])
last_read_line_b_temp = int(last_read_line_list[1])
if control_code_mode:
last_read_line_a = last_read_line_a_temp - 1
last_read_line_b = last_read_line_b_temp - 1
control_code_mode = False
if first_page:
first_page = False
origin_post.append(last_screen)
else:
# 這裡是根據觀察畫面行數的變化歸納出的神奇公式...
# 輸出的結果是要判斷出畫面的最後 x 行是新的文章內容
#
# 這裡是 PyPtt 最黑暗最墮落的地方,所有你所知的程式碼守則,在這裡都不適用
# 每除完一次錯誤,我會陷入嚴重的創傷後壓力症候群,而我的腦袋會自動選擇遺忘這裡所有的一切
# 以確保下一個週一,我可以正常上班
# but it works!
# print(LastScreen)
# print(f'last_read_line_a_temp [{last_read_line_a_temp}]')
# print(f'last_read_line_b_temp [{last_read_line_b_temp}]')
# print(f'last_read_line_a {last_read_line_a}')
# print(f'last_read_line_b {last_read_line_b}')
# print(f'GetLineB {last_read_line_a_temp - last_read_line_a}')
# print(f'GetLineA {last_read_line_b_temp - last_read_line_b}')
# print(f'show line {last_read_line_b_temp - last_read_line_a_temp + 1}')
if not control_code_mode:
if last_read_line_a_temp in stop_dict:
new_content_part = '\n'.join(
lines[-stop_dict[last_read_line_a_temp]:])
stop_dict = dict()
else:
get_line_b = last_read_line_b_temp - last_read_line_b
if get_line_b > 0:
# print('Type 1')
new_content_part = '\n'.join(lines[-get_line_b:])
if index == 1 and len(new_content_part) == get_line_b - 1:
new_content_part = '\n'.join(lines[-(get_line_b * 2):])
elif origin_post:
last_line_temp = origin_post[-1].strip()
try_line = lines[-(get_line_b + 1)].strip()
if not last_line_temp.endswith(try_line):
new_content_part = try_line + '\n' + new_content_part
stop_dict = dict()
else:
# 駐足現象,LastReadLineB跟上一次相比並沒有改變
if (last_read_line_b_temp + 1) not in stop_dict:
stop_dict[last_read_line_b_temp + 1] = 1
stop_dict[last_read_line_b_temp + 1] += 1
get_line_a = last_read_line_a_temp - last_read_line_a
if get_line_a > 0:
# print(f'Type 2 get_line_a [{get_line_a}]')
new_content_part = '\n'.join(lines[-get_line_a:])
else:
new_content_part = '\n'.join(lines)
else:
new_content_part = lines[-1]
origin_post.append(new_content_part)
log.logger.debug('NewContentPart', new_content_part)
if index == 1:
if content_start_jump and len(new_content_part) == 0:
get_line_b += 1
new_content_part = '\n'.join(lines[-get_line_b:])
origin_post.pop()
origin_post.append(new_content_part)
break
if not control_code_mode:
last_read_line_a = last_read_line_a_temp
last_read_line_b = last_read_line_b_temp
for EC in screens.Target.content_end_list:
if EC in last_screen:
push_start = True
break
if push_start:
cmd = command.right
else:
cmd = command.down
# print(api.Unconfirmed)
origin_post = '\n'.join(origin_post)
# OriginPost = [line.strip() for line in OriginPost.split('\n')]
# OriginPost = '\n'.join(OriginPost)
log.logger.debug('OriginPost', origin_post)
return origin_post, has_control_code
mail_capacity: Optional[tuple[int, int]] = None
def get_mailbox_capacity(api) -> tuple[int, int]:
global mail_capacity
if mail_capacity is not None:
return mail_capacity
last_screen = api.connect_core.get_screen_queue()[-1]
capacity_line = last_screen.split('\n')[2]
log.logger.debug('capacity_line', capacity_line)
pattern_result = re.compile('(\d+)/(\d+)').search(capacity_line)
if pattern_result is not None:
current_capacity = int(pattern_result.group(0).split('/')[0])
max_capacity = int(pattern_result.group(0).split('/')[1])
log.logger.debug('current_capacity', current_capacity)
log.logger.debug('max_capacity', max_capacity)
mail_capacity = (current_capacity, max_capacity)
return current_capacity, max_capacity
return 0, 0
# > 1 112/09 ericsk □ [心得] 終於開板了
# ┌── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─┐
# │ 文章代碼(AID): #13cPSYOX (Python) [ptt.cc] [心得] 終於開板了 │
# │ 文章網址: https://www.ptt.cc/bbs/Python/M.1134139170.A.621.html │
# │ 這一篇文章值 2 Ptt幣 │
# └── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─┘
def parse_query_post(api, ori_screen):
lock_post = False
try:
cursor_line = [line for line in ori_screen.split(
'\n') if line.strip().startswith(api.cursor)][0]
except Exception as e:
print(api.cursor)
print(ori_screen)
raise e
post_author = cursor_line
if '□' in post_author:
post_author = post_author[:post_author.find('□')].strip()
elif 'R:' in post_author:
post_author = post_author[:post_author.find('R:')].strip()
elif ' 轉 ' in post_author:
post_author = post_author[:post_author.find('轉')].strip()
elif ' 鎖 ' in post_author:
post_author = post_author[:post_author.find('鎖')].strip()
lock_post = True
post_author = post_author[post_author.rfind(' '):].strip()
post_title = cursor_line
if ' □ ' in post_title:
post_title = post_title[post_title.find('□') + 1:].strip()
elif ' R:' in post_title:
post_title = post_title[post_title.find('R:'):].strip()
elif ' 轉 ' in post_title:
post_title = post_title[post_title.find('轉') + 1:].strip()
post_title = f'Fw: {post_title}'
elif ' 鎖 ' in post_title:
post_title = post_title[post_title.find('鎖') + 1:].strip()
ori_screen_temp = ori_screen[ori_screen.find('┌──'):]
ori_screen_temp = ori_screen_temp[:ori_screen_temp.find('└──')]
aid_line = [line for line in ori_screen.split(
'\n') if line.startswith('│ 文章代碼(AID)')]
post_aid = None
if len(aid_line) == 1:
aid_line = aid_line[0]
pattern = re.compile('#[\w|-]+')
pattern_result = pattern.search(aid_line)
post_aid = pattern_result.group(0)[1:]
pattern = re.compile('文章網址: https:[\S]+html')
pattern_result = pattern.search(ori_screen_temp)
if pattern_result is None:
post_web = None
else:
post_web = pattern_result.group(0)[6:]
pattern = re.compile('這一篇文章值 [\d]+ Ptt幣')
pattern_result = pattern.search(ori_screen_temp)
if pattern_result is None:
# 特殊文章無價格
post_money = -1
else:
post_money = pattern_result.group(0)[7:]
post_money = post_money[:post_money.find(' ')]
post_money = int(post_money)
pattern = re.compile('[\d]+\/[\d]+')
pattern_result = pattern.search(cursor_line)
if pattern_result is None:
list_date = None
else:
list_date = pattern_result.group(0)
list_date = list_date[-5:]
# print(list_date)
# > 7485 9 8/09 CodingMan □ [閒聊] PTT Library 更新
# > 79189 M 1 9/17 LittleCalf □ [公告] 禁言退文公告
# >781508 +爆 9/17 jodojeda □ [新聞] 國人吃魚少 學者:應把吃魚當成輕鬆愉快
# >781406 +X1 9/17 kingofage111 R: [申請] ReDmango 請辭Gossiping板主職務
pattern = re.compile('[\d]+')
pattern_result = pattern.search(cursor_line)
post_index = 0
if pattern_result is not None:
post_index = int(pattern_result.group(0))
push_number = cursor_line
push_number = push_number[7:11]
push_number = push_number.split(' ')
push_number = list(filter(None, push_number))
if len(push_number) == 0:
push_number = None
else:
push_number = push_number[-1]
if push_number.startswith('爆') or push_number.startswith('~爆'):
push_number = '爆'
if push_number.startswith('+') or push_number.startswith('~'):
push_number = push_number[1:]
if push_number.lower().startswith('m'):
push_number = push_number[1:]
if push_number.lower().startswith('!'):
push_number = push_number[1:]
if push_number.lower().startswith('s'):
push_number = push_number[1:]
if push_number.lower().startswith('='):
push_number = push_number[1:]
if len(push_number) == 0:
push_number = None
log.logger.debug('PostAuthor', post_author)
log.logger.debug('PostTitle', post_title)
log.logger.debug('PostAID', post_aid)
log.logger.debug('PostWeb', post_web)
log.logger.debug('PostMoney', post_money)
log.logger.debug('ListDate', list_date)
log.logger.debug('PushNumber', push_number)
return lock_post, post_author, post_title, post_aid, post_web, post_money, list_date, push_number, post_index
def get_search_condition_cmd(index_type: data_type.NewIndex, search_list: Optional[list] = None):
cmd_list = []
if not search_list:
return cmd_list
for search_type, search_condition in search_list:
if search_type == data_type.SearchType.KEYWORD:
cmd_list.append('/')
elif search_type == data_type.SearchType.AUTHOR:
cmd_list.append('a')
elif search_type == data_type.SearchType.MARK:
cmd_list.append('G')
elif index_type == data_type.NewIndex.BOARD:
if search_type == data_type.SearchType.COMMENT:
cmd_list.append('Z')
elif search_type == data_type.SearchType.MONEY:
cmd_list.append('A')
else:
continue
else:
continue
cmd_list.append(search_condition)
cmd_list.append(command.enter)
return cmd_list
def goto_board(api, board: str, refresh: bool = False, end: bool = False) -> None:
cmd_list = []
cmd_list.append(command.go_main_menu)
cmd_list.append('qs')
cmd_list.append(board)
cmd_list.append(command.enter)
cmd_list.append(command.space)
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit('任意鍵', log_level=log.DEBUG, response=' '),
connect_core.TargetUnit('互動式動畫播放中', log_level=log.DEBUG, response=command.ctrl_c),
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
]
if refresh:
current_refresh = True
else:
if board.lower() in api._goto_board_list:
current_refresh = True
else:
current_refresh = False
api._goto_board_list.append(board.lower())
api.connect_core.send(cmd, target_list, refresh=current_refresh)
if end:
cmd_list = []
cmd_list.append('1')
cmd_list.append(command.enter)
cmd_list.append('$')
cmd = ''.join(cmd_list)
target_list = [
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True),
]
api.connect_core.send(cmd, target_list)
def one_thread(api):
current_thread_id = threading.get_ident()
if current_thread_id != api._thread_id:
raise exceptions.MultiThreadOperated()
@functools.lru_cache(maxsize=64)
def check_board(api, board: str, check_moderator: bool = False) -> Dict:
if board.lower() not in api._exist_board_list:
board_info = _api_get_board_info.get_board_info(api, board, get_post_kind=False, call_by_others=False)
api._exist_board_list.append(board.lower())
api._board_info_list[board.lower()] = board_info
moderators = board_info[data_type.BoardField.moderators]
moderators = [x.lower() for x in moderators]
api._moderators[board.lower()] = moderators
api._board_info_list[board.lower()] = board_info
if check_moderator:
if api.ptt_id.lower() not in api._moderators[board.lower()]:
raise exceptions.NeedModeratorPermission(board)
return api._board_info_list[board.lower()]
================================================
FILE: PyPtt/check_value.py
================================================
from . import i18n
from . import log
def check_type(value, value_type, name) -> None:
if not isinstance(value, value_type):
if value_type is str:
raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_string}, but got {value}')
elif value_type is int:
raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_integer}, but got {value}')
elif value_type is bool:
raise TypeError(f'[PyPtt] {name} {i18n.must_be_a_boolean}, but got {value}')
else:
raise TypeError(f'[PyPtt] {name} {i18n.must_be} {value_type}, but got {value}')
def check_range(value, min_value, max_value, name) -> None:
check_type(value, int, name)
check_type(min_value, int, 'min_value')
check_type(max_value, int, 'max_value')
if min_value <= value <= max_value:
return
raise ValueError(f'{name} {value} {i18n.must_between} {min_value} ~ {max_value}')
def check_index(name, index, max_value=None) -> None:
check_type(index, int, name)
if index < 1:
raise ValueError(f'{name} {i18n.must_bigger_than} 0')
if max_value is not None:
if index > max_value:
log.logger.info('index', index)
log.logger.info('max_value', max_value)
raise ValueError(f'{name} {index} {i18n.must_between} 0 ~ {max_value}')
def check_index_range(start_name, start_index, end_name, end_index, max_value=None) -> None:
check_type(start_index, int, start_name)
check_type(end_index, int, end_name)
if start_index < 1:
raise ValueError(f'{start_name} {start_index} {i18n.must_bigger_than} 0')
if end_index <= 1:
raise ValueError(f'{end_name} {end_index} {i18n.must_bigger_than} 1')
if start_index > end_index:
raise ValueError(f'{end_name} {end_index} {i18n.must_bigger_than} {start_name} {start_index}')
if max_value is not None:
if start_index > max_value:
raise ValueError(f'{start_name} {start_index} {i18n.must_small_than} {max_value}')
if end_index > max_value:
raise ValueError(f'{end_name} {end_index} {i18n.must_small_than} {max_value}')
if __name__ == '__main__':
QQ = str
if QQ is str:
print('1')
if QQ == str:
print('2')
if isinstance('', QQ):
print('3')
================================================
FILE: PyPtt/command.py
================================================
# http://www.physics.udel.edu/~watson/scen103/ascii.html
enter = '\r'
tab = '\t'
ctrl_c = '\x03'
ctrl_d = '\x04'
ctrl_e = '\x05'
ctrl_f = '\x06'
ctrl_h = '\x08'
ctrl_l = '\x0C'
ctrl_p = '\x10'
ctrl_u = '\x15'
ctrl_x = '\x18'
ctrl_y = '\x19'
ctrl_z = '\x1A'
star = '\x2A'
up = '\x1b\x4fA'
down = '\x1b\x4fB'
right = '\x1b\x4fC'
left = '\x1b\x4fD'
space = ' '
query_post = 'Q'
comment = 'X'
go_main_menu = ' ' + left * 5
go_main_menu_type_q = 'q' * 5
refresh = ctrl_l
control_code = ctrl_u + star
backspace = ctrl_h
================================================
FILE: PyPtt/config.py
================================================
from . import data_type
from . import log
class Config:
# retry_wait_time 秒後重新連線
retry_wait_time = 3
# ScreenLTimeOut 秒後判定此畫面沒有可辨識的目標
screen_timeout = 3.0
# screen_long_timeout 秒後判定此畫面沒有可辨識的目標
# 適用於需要特別等待的情況,例如: 剔除其他登入等等
# 建議不要低於 10 秒,剔除其他登入最長可能會花費約六到七秒
screen_long_timeout = 10.0
# screen_post_timeout 秒後判定此畫面沒有可辨識的目標
# 適用於貼文等待的情況,建議不要低於 60 秒
screen_post_timeout = 60.0
# 預設語言
language = data_type.Language.MANDARIN
# 預設 log 等級
log_level = log.INFO
# 預設不剔除其他登入
kick_other_session = False
# 預設登入 PTT1
host = data_type.HOST.PTT1
# 預設採用 websockets
connect_mode = None
# 預設使用 23
port = 23
logger_callback = None
LOGGER_CONFIG = {
}
================================================
FILE: PyPtt/connect_core.py
================================================
from __future__ import annotations
import asyncio
import ssl
import telnetlib
import threading
import time
import traceback
import warnings
from typing import Any
import websockets
import websockets.exceptions
import websockets.http
import PyPtt
from . import command
from . import data_type
from . import exceptions
from . import i18n
from . import log
from . import screens
websockets.http.USER_AGENT += f' PyPtt/{PyPtt.__version__}'
ssl_context = ssl.create_default_context()
class TargetUnit:
def __init__(self, detect_target, log_level: log.LogLevel = None, response: [Any | str] = '', break_detect=False,
break_detect_after_send=False, exceptions_=None, refresh=True, secret=False, handler=None,
max_match: int = 0):
self.detect_target = detect_target
if log_level is None:
self.log_level = log.INFO
else:
self.log_level = log_level
self._response_func = response
self._break_detect = break_detect
self._exception = exceptions_
self._refresh = refresh
self._break_after_send = break_detect_after_send
self._secret = secret
self._Handler = handler
self._max_match = max_match
self._current_match = 0
def is_match(self, screen: str) -> bool:
if self._current_match >= self._max_match > 0:
return False
if isinstance(self.detect_target, str):
if self.detect_target in screen:
self._current_match += 1
return True
return False
elif isinstance(self.detect_target, list):
for Target in self.detect_target:
if Target not in screen:
return False
self._current_match += 1
return True
def get_detect_target(self):
return self.detect_target
def get_log_level(self):
return self.log_level
def get_response(self, screen: str) -> str:
if callable(self._response_func):
return self._response_func(screen)
return self._response_func
def is_break(self) -> bool:
return self._break_detect
def raise_exception(self):
if isinstance(self._exception, Exception):
raise self._exception
def is_refresh(self) -> bool:
return self._refresh
def is_break_after_send(self) -> bool:
return self._break_after_send
def is_secret(self) -> bool:
return self._secret
class RecvData:
def __init__(self):
self.data = None
async def websocket_recv_func(core, recv_data_obj):
recv_data_obj.data = await core.recv()
async def websocket_receiver(core, screen_timeout, recv_data_obj):
# Wait for at most 1 second
await asyncio.wait_for(
websocket_recv_func(core, recv_data_obj),
timeout=screen_timeout)
class ReceiveDataQueue(object):
def __init__(self):
self._ReceiveDataQueue = []
def add(self, screen):
self._ReceiveDataQueue.append(screen)
self._ReceiveDataQueue = self._ReceiveDataQueue[-10:]
def get(self, last=1):
return self._ReceiveDataQueue[-last:]
class API(object):
def __init__(self, config):
self.current_encoding = 'big5uao'
self.config = config
self._RDQ = ReceiveDataQueue()
self._UseTooManyResources = TargetUnit(screens.Target.use_too_many_resources,
exceptions_=exceptions.UseTooManyResources())
def connect(self) -> None:
def _wait():
for i in range(self.config.retry_wait_time):
if self.config.host == data_type.HOST.PTT1:
log.logger.info(i18n.prepare_connect_again, i18n.PTT, str(self.config.retry_wait_time - i))
elif self.config.host == data_type.HOST.PTT2:
log.logger.info(i18n.prepare_connect_again, i18n.PTT2, str(self.config.retry_wait_time - i))
elif self.config.host == data_type.HOST.LOCALHOST:
log.logger.info(i18n.prepare_connect_again, i18n.localhost, str(self.config.retry_wait_time - i))
else:
log.logger.info(i18n.prepare_connect_again, self.config.host, str(self.config.retry_wait_time - i))
time.sleep(1)
warnings.filterwarnings("ignore", category=DeprecationWarning)
self.current_encoding = 'big5uao'
# self.log.py.info(i18n.connect_core, i18n.active)
if self.config.host == data_type.HOST.PTT1:
telnet_host = 'ptt.cc'
websocket_host = 'wss://ws.ptt.cc/bbs/'
websocket_origin = 'https://term.ptt.cc'
elif self.config.host == data_type.HOST.PTT2:
telnet_host = 'ptt2.cc'
websocket_host = 'wss://ws.ptt2.cc/bbs/'
websocket_origin = 'https://term.ptt2.cc'
elif self.config.host == data_type.HOST.LOCALHOST:
telnet_host = 'localhost'
websocket_host = 'wss://localhost'
websocket_origin = 'https://term.ptt.cc'
else:
telnet_host = self.config.host
websocket_host = f'wss://{self.config.host}'
websocket_origin = 'https://term.ptt.cc'
connect_success = False
for _ in range(2):
try:
if self.config.connect_mode == data_type.ConnectMode.TELNET:
self._core = telnetlib.Telnet(telnet_host, self.config.port)
else:
if not threading.current_thread() is threading.main_thread():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
log.logger.debug('USER_AGENT', websockets.http.USER_AGENT)
self._core = asyncio.get_event_loop().run_until_complete(
websockets.connect(
websocket_host,
origin=websocket_origin,
ssl=ssl_context))
connect_success = True
except Exception as e:
traceback.print_tb(e.__traceback__)
print(e)
if self.config.host == data_type.HOST.PTT1:
log.logger.info(i18n.connect, i18n.PTT, i18n.fail)
elif self.config.host == data_type.HOST.PTT2:
log.logger.info(i18n.connect, i18n.PTT2, i18n.fail)
elif self.config.host == data_type.HOST.LOCALHOST:
log.logger.info(i18n.connect, i18n.localhost, i18n.fail)
else:
log.logger.info(i18n.connect, self.config.host, i18n.fail)
_wait()
continue
break
if not connect_success:
raise exceptions.ConnectError(self.config)
def _decode_screen(self, receive_data_buffer, start_time, target_list, is_secret, refresh, msg):
break_detect_after_send = False
use_too_many_res = False
vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding)
screen = vt100_p.screen
find_target = False
target_index = -1
for target in target_list:
condition = target.is_match(screen)
if condition:
if target._Handler is not None:
target._Handler(screen)
if len(screen) > 0:
screens.show(self.config, screen)
self._RDQ.add(screen)
if target == self._UseTooManyResources:
use_too_many_res = True
# print(f'1 {use_too_many_res}')
break
target.raise_exception()
find_target = True
end_time = time.time()
log.logger.debug(i18n.spend_time, round(end_time - start_time, 3))
if target.is_break():
target_index = target_list.index(target)
break
msg = target.get_response(screen)
add_refresh = False
if target.is_refresh():
add_refresh = True
elif refresh:
add_refresh = True
if add_refresh:
if not msg.endswith(command.refresh):
msg = msg + command.refresh
is_secret = target.is_secret()
if target.is_break_after_send():
# break_index = target_list.index(target)
break_detect_after_send = True
break
return screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index
def send(self, msg: str, target_list: list, screen_timeout: int = 0, refresh: bool = True,
secret: bool = False) -> int:
if not all(isinstance(T, TargetUnit) for T in target_list):
raise ValueError('Item of TargetList must be TargetUnit')
if self._UseTooManyResources not in target_list:
target_list.append(self._UseTooManyResources)
if screen_timeout == 0:
current_screen_timeout = self.config.screen_timeout
else:
current_screen_timeout = screen_timeout
break_detect_after_send = False
is_secret = secret
use_too_many_res = False
while True:
if refresh and not msg.endswith(command.refresh):
msg = msg + command.refresh
try:
msg = msg.encode('utf-8', 'replace')
except AttributeError:
pass
except Exception as e:
traceback.print_tb(e.__traceback__)
print(e)
msg = msg.encode('utf-8', 'replace')
if is_secret:
log.logger.debug(i18n.send_msg, i18n.hide_sensitive_info)
else:
log.logger.debug(i18n.send_msg, str(msg))
if self.config.connect_mode == data_type.ConnectMode.TELNET:
try:
self._core.read_very_eager()
self._core.write(msg)
except EOFError:
raise exceptions.ConnectionClosed()
else:
try:
asyncio.get_event_loop().run_until_complete(
self._core.send(msg))
except websockets.exceptions.ConnectionClosedError:
raise exceptions.ConnectionClosed()
except RuntimeError:
raise exceptions.ConnectionClosed()
except websockets.exceptions.ConnectionClosedOK:
raise exceptions.ConnectionClosed()
if break_detect_after_send:
return -1
msg = ''
receive_data_buffer = bytes()
start_time = time.time()
mid_time = time.time()
while mid_time - start_time < current_screen_timeout:
# print(1)
recv_data_obj = RecvData()
if self.config.connect_mode == data_type.ConnectMode.TELNET:
try:
recv_data_obj.data = self._core.read_very_eager()
except EOFError:
return -1
else:
try:
asyncio.get_event_loop().run_until_complete(
websocket_receiver(
self._core, current_screen_timeout, recv_data_obj))
except websockets.exceptions.ConnectionClosed:
if use_too_many_res:
raise exceptions.UseTooManyResources()
raise exceptions.ConnectionClosed()
except websockets.exceptions.ConnectionClosedOK:
raise exceptions.ConnectionClosed()
except asyncio.TimeoutError:
return -1
except RuntimeError:
raise exceptions.ConnectionClosed()
receive_data_buffer += recv_data_obj.data
screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)
if self.current_encoding == 'big5uao' and not find_target:
self.current_encoding = 'utf-8'
screen_, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)
if find_target:
screen = screen_
else:
self.current_encoding = 'big5uao'
# print(4)
if target_index != -1:
return target_index
if use_too_many_res:
continue
if find_target:
break
if len(screen) > 0:
screens.show(self.config, screen)
self._RDQ.add(screen)
# print(6)
mid_time = time.time()
if not find_target:
return -1
return -2
def close(self):
if self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:
asyncio.get_event_loop().run_until_complete(self._core.close())
else:
self._core.close()
def get_screen_queue(self) -> list:
return self._RDQ.get(1)
================================================
FILE: PyPtt/data_type.py
================================================
import time
from enum import auto
from AutoStrEnum import AutoStrEnum
class Language:
MANDARIN = 'zh_TW'
ENGLISH = 'en_US'
class ConnectMode(AutoStrEnum):
TELNET = auto()
WEBSOCKETS = auto()
class SearchType(AutoStrEnum):
"""文章搜尋類型"""
NOPE = auto()
# 搜尋關鍵字 / ?
KEYWORD = auto()
# 搜尋作者 a
AUTHOR = auto()
# 搜尋推文數 Z
COMMENT = auto()
# 搜尋標記 G
MARK = auto()
# 搜尋稿酬 A
MONEY = auto()
class ReplyTo(AutoStrEnum):
# 回文類型
BOARD = auto()
MAIL = auto()
BOARD_MAIL = auto()
class CommentType(AutoStrEnum):
PUSH = auto()
BOO = auto()
ARROW = auto()
class UserField(AutoStrEnum):
ptt_id = auto()
money = auto()
login_count = auto()
account_verified = auto()
legal_post = auto()
illegal_post = auto()
activity = auto()
mail = auto()
last_login_date = auto()
last_login_ip = auto()
five_chess = auto()
chess = auto()
signature_file = auto()
class CommentField(AutoStrEnum):
type = auto()
author = auto()
content = auto()
ip = auto()
time = auto()
class PostStatus(AutoStrEnum):
EXISTS = auto()
DELETED_BY_AUTHOR = auto()
DELETED_BY_MODERATOR = auto()
DELETED_BY_UNKNOWN = auto()
class PostField(AutoStrEnum):
board = auto()
aid = auto()
index = auto()
author = auto()
date = auto()
title = auto()
content = auto()
money = auto()
url = auto()
ip = auto()
comments = auto()
post_status = auto()
list_date = auto()
has_control_code = auto()
pass_format_check = auto()
location = auto()
push_number = auto()
is_lock = auto()
full_content = auto()
is_unconfirmed = auto()
# class WaterballInfo:
# def __init__(self, waterball_type, target, content, date):
# self.type: int = parse_para(int, waterball_type)
# self.target: str = parse_para(str, target)
# self.content: str = parse_para(str, content)
# self.date: str = parse_para(str, date)
class Cursor:
# 舊式游標
OLD: str = '●'
# 新式游標
NEW: str = '>'
class NewIndex(AutoStrEnum):
# 看板
BOARD = auto()
# 信箱
MAIL = auto()
# 網頁,尚不支援
# WEB = auto()
class HOST(AutoStrEnum):
# 批踢踢萬
PTT1 = auto()
# 批踢踢兔
PTT2 = auto()
# 本機測試用
LOCALHOST = auto()
class MarkType(AutoStrEnum):
# s 文章
S = auto()
# 標記文章
D = auto()
# 刪除標記文章
DELETE_D = auto()
# M 起來
M = auto()
# 待證實文章
UNCONFIRMED = auto()
class FavouriteBoardField(AutoStrEnum):
board = auto()
type = auto()
title = auto()
class MailField(AutoStrEnum):
origin_mail = auto()
author = auto()
title = auto()
date = auto()
content = auto()
ip = auto()
location = auto()
is_red_envelope = auto()
class BoardField(AutoStrEnum):
board = auto()
online_user = auto()
mandarin_des = auto()
moderators = auto()
open_status = auto()
into_top_ten_when_hide = auto()
can_non_board_members_post = auto()
can_reply_post = auto()
self_del_post = auto()
can_comment_post = auto()
can_boo_post = auto()
can_fast_push = auto()
min_interval_between_comments = auto()
is_comment_record_ip = auto()
is_comment_aligned = auto()
can_moderators_del_illegal_content = auto()
does_tran_post_auto_recorded_and_require_post_permissions = auto()
is_cool_mode = auto()
is_require18 = auto()
require_login_time = auto()
require_illegal_post = auto()
# post_kind = auto()
post_kind_list = auto()
class Compare(AutoStrEnum):
BIGGER = auto()
SAME = auto()
SMALLER = auto()
UNKNOWN = auto()
class TimedDict:
def __init__(self, timeout: int = 0):
self.timeout = timeout
self.data = {}
self.timestamps = {}
def __setitem__(self, key, value):
self.data[key] = value
self.timestamps[key] = time.time()
def __getitem__(self, key):
if key not in self.data:
raise KeyError(key)
timestamp = self.timestamps[key]
if time.time() - timestamp > self.timeout > 0:
del self.data[key]
del self.timestamps[key]
raise KeyError(key)
return self.data[key]
def __contains__(self, key):
try:
self[key]
except KeyError:
return False
else:
return True
def __len__(self):
self.cleanup()
return len(self.data)
def cleanup(self):
if self.timeout == 0:
return
now = time.time()
to_remove = [key for key, timestamp in self.timestamps.items()
if now - timestamp > self.timeout > 0]
for key in to_remove:
del self.data[key]
del self.timestamps[key]
================================================
FILE: PyPtt/exceptions.py
================================================
from . import data_type
from . import i18n
class Error(Exception):
pass
class UnknownError(Error):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
class RequireLogin(Error):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
class NoPermission(Error):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
class LoginError(Error):
def __init__(self):
self.message = i18n.login_fail
def __str__(self):
return self.message
class NoFastComment(Error):
def __init__(self):
self.message = i18n.no_fast_comment
def __str__(self):
return self.message
class NoSuchUser(Error):
def __init__(self, user):
self.message = i18n.no_such_user + ': ' + user
def __str__(self):
return self.message
class NoSuchMail(Error):
def __init__(self):
self.message = i18n.no_such_mail
def __str__(self):
return self.message
# class UserOffline(Error):
# def __init__(self, user):
# self.message = i18n.user_offline + ': ' + user
#
# def __str__(self):
# return self.message
# class ParseError(Error):
# def __init__(self, screen):
# self.message = screen
#
# def __str__(self):
# return self.message
class NoMoney(Error):
def __init__(self):
self.message = i18n.no_money
def __str__(self):
return self.message
class NoSuchBoard(Error):
def __init__(self, config, board):
if config.host == data_type.HOST.PTT1:
self.message = [
i18n.PTT,
i18n.no_such_board
]
else:
self.message = [
i18n.PTT2,
i18n.no_such_board
]
if config.language == data_type.Language.MANDARIN:
self.message = ''.join(self.message) + ': ' + board
else:
self.message = ' '.join(self.message) + ': ' + board
def __str__(self):
return self.message
class ConnectionClosed(Error):
def __init__(self):
self.message = i18n.connection_closed
def __str__(self):
return self.message
class UnregisteredUser(Error):
def __init__(self, api_name):
self.message = i18n.unregistered_user_cant_use_this_api + ': ' + api_name
def __str__(self):
return self.message
class MultiThreadOperated(Error):
def __init__(self):
self.message = i18n.multi_thread_operate
def __str__(self):
return self.message
class WrongIDorPassword(Error):
def __init__(self):
self.message = i18n.wrong_id_pw
def __str__(self):
return self.message
class WrongPassword(Error):
def __init__(self):
self.message = i18n.error_pw
def __str__(self):
return self.message
class LoginTooOften(Error):
def __init__(self):
self.message = i18n.login_too_often
def __str__(self):
return self.message
class UseTooManyResources(Error):
def __init__(self):
self.message = i18n.use_too_many_resources
def __str__(self):
return self.message
class HostNotSupport(Error):
def __init__(self, api):
self.message = f'{i18n.ptt2_not_support}: {api}'
def __str__(self):
return self.message
class CantComment(Error):
def __init__(self):
self.message = i18n.no_comment
def __str__(self):
return self.message
class CantResponse(Error):
def __init__(self):
self.message = i18n.no_response
def __str__(self):
return self.message
class NeedModeratorPermission(Error):
def __init__(self, board):
self.message = f'{i18n.need_moderator_permission}: {board}'
def __str__(self):
return self.message
class ConnectError(Error):
def __init__(self, config):
self.message = i18n.connect_fail
def __str__(self):
return self.message
class NoSuchPost(Error):
def __init__(self, board, aid):
self.message = i18n.replace(
i18n.no_such_post,
board,
aid)
def __str__(self):
return self.message
class CanNotUseSearchPostCode(Error):
"""
此狀態下無法使用搜尋文章代碼(AID)功能
"""
def __init__(self):
self.message = i18n.can_not_use_search_post_code_f
def __str__(self):
return self.message
class UserHasPreviouslyBeenBanned(Error):
def __init__(self):
self.message = i18n.user_has_previously_been_banned
def __str__(self):
return self.message
class MailboxFull(Error):
def __init__(self):
self.message = i18n.mail_box_full
def __str__(self):
return self.message
class NoSearchResult(Error):
def __init__(self):
self.message = i18n.no_search_result
def __str__(self):
return self.message
# 此帳號已設定為只能使用安全連線
class OnlySecureConnection(Error):
def __init__(self):
self.message = i18n.only_secure_connection
def __str__(self):
return self.message
class SetContactMailFirst(Error):
def __init__(self):
self.message = i18n.set_contact_mail_first
def __str__(self):
return self.message
class ResetYourContactEmail(Error):
def __init__(self):
self.message = i18n.reset_your_contact_email
def __str__(self):
return self.message
================================================
FILE: PyPtt/i18n.py
================================================
import os
import random
import yaml
from . import __version__
from . import data_type
locale_pool = {
data_type.Language.ENGLISH,
data_type.Language.MANDARIN
}
_script_path = os.path.dirname(os.path.abspath(__file__))
_lang_data = {}
mapping = {
'{version}': __version__,
}
def replace(string, *args):
for i in range(len(args)):
target = f'{args[i]}'
string = string.replace(f'_target{i}_', target)
return string
def init(locale: str, cache: bool = False) -> None:
if locale not in locale_pool:
raise ValueError(f'Unknown locale: {locale}')
language_file = f'{_script_path}/lang/{locale}.yaml'
if not os.path.exists(language_file):
raise ValueError(f'Unknown locale file: {language_file}')
with open(language_file, "r") as f:
string_data = yaml.safe_load(f)
for k, v in string_data.items():
if isinstance(v, list):
v = random.choice(v)
elif isinstance(v, str):
pass
else:
raise ValueError(f'Unknown string data type: {v}')
if locale == data_type.Language.ENGLISH:
v = v[0].upper() + v[1:]
for mk, mv in mapping.items():
v = v.replace(mk, mv)
globals()[k] = v
if cache:
global _lang_data
_lang_data[k] = v
================================================
FILE: PyPtt/lang/en_US.yaml
================================================
PTT: PTT
PTT2: PTT2
active: Active
author: Author
board: Board
can_not_use_search_post_code_f: This status can not use the search PostField code function
catch_bottom_post_success: Catch bottom post success
change_pw: Change password
comment: Comment
comment_content: Comment content
comment_date: Comment date
comment_id: Comment id
connect: Connect
connect_core: Connect core
connect_fail: Connect fail
connect_mode_TELNET: Telnet
connect_mode_WEBSOCKET: WebSocket
connection_closed: Connection Closed
content: Content
current_version: Current version
date: Date
delete_post: Delete post
development_version: Running development version
done: Done
english_module: English
error_pw: Wrong password
fail: Fail
find_newest_index: Find newest index
get_board_info: Get board info _target0_
get_favourite_board_list: Query favourite board list
get_mail: Get mail
give_money_to: give _target0_ _target1_ P coins
goodbye:
- good bye
- bye
- see you
- catch you later
- I hate to run, but...
- Until we meet again, I will wait
has_comment_permission: User has permission to comment
has_new_mail_goto_main_menu: New mail! Back to main menu
has_post_permission: Have permission to post
hide_sensitive_info: Hide sensitive info
in_login_process_please_wait: In login process, please wait
initialization: Init
kick_other_login: Kick other login
latest_version: Running the latest version
localhost: Localhost
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
SYMBOL INDEX (299 symbols across 60 files)
FILE: PyPtt/PTT.py
class API (line 37) | class API:
method __init__ (line 38) | def __init__(self, **kwargs):
method __del__ (line 198) | def __del__(self):
method login (line 202) | def login(self, ptt_id: str, ptt_pw: str, kick_other_session: bool = F...
method logout (line 242) | def logout(self) -> None:
method get_time (line 264) | def get_time(self) -> str:
method get_post (line 287) | def get_post(self, board: str, aid: Optional[str] = None, index: Optio...
method get_newest_index (line 354) | def get_newest_index(self, index_type: data_type.NewIndex, board: Opti...
method post (line 422) | def post(self, board: str, title_index: int, title: str, content: str,...
method comment (line 458) | def comment(self, board: str, comment_type: data_type.CommentType, con...
method get_user (line 499) | def get_user(self, user_id: str) -> Dict:
method give_money (line 532) | def give_money(self, ptt_id: str, money: int, red_bag_title: Optional[...
method mail (line 573) | def mail(self, ptt_id: str, title: str, content: str, sign_file: [int ...
method get_all_boards (line 609) | def get_all_boards(self) -> List[str]:
method reply_post (line 635) | def reply_post(self, reply_to: data_type.ReplyTo, board: str, content:...
method set_board_title (line 676) | def set_board_title(self, board: str, new_title: str) -> None:
method mark_post (line 709) | def mark_post(self, mark_type: int, board: str, aid: Optional[str] = N...
method get_favourite_boards (line 747) | def get_favourite_boards(self) -> List[dict]:
method bucket (line 773) | def bucket(self, board: str, bucket_days: int, reason: str, ptt_id: st...
method search_user (line 809) | def search_user(self, ptt_id: str, min_page: Optional[int] = None, max...
method get_board_info (line 840) | def get_board_info(self, board: str, get_post_types: bool = False) -> ...
method get_mail (line 871) | def get_mail(self, index: int, search_type: Optional[data_type.SearchT...
method del_mail (line 908) | def del_mail(self, index: int) -> None:
method change_pw (line 940) | def change_pw(self, new_password: str) -> None:
method get_aid_from_url (line 972) | def get_aid_from_url(self, url: str) -> Tuple[str, str]:
method get_bottom_post_list (line 993) | def get_bottom_post_list(self, board: str) -> List[str]:
method del_post (line 1023) | def del_post(self, board: str, aid: Optional[str] = None, index: int =...
method fast_post_step0 (line 1057) | def fast_post_step0(self, board: str, title: str, content: str, post_t...
method fast_post_step1 (line 1060) | def fast_post_step1(self, sign_file):
FILE: PyPtt/_api_bucket.py
function bucket (line 11) | def bucket(api, board: str, bucket_days: int, reason: str, ptt_id: str) ...
FILE: PyPtt/_api_call_status.py
function get_call_status (line 9) | def get_call_status(api) -> None:
function set_call_status (line 51) | def set_call_status(api, call_status) -> None:
FILE: PyPtt/_api_change_pw.py
function change_pw (line 8) | def change_pw(api, new_password: str) -> None:
FILE: PyPtt/_api_comment.py
function _comment (line 23) | def _comment(api,
function comment (line 122) | def comment(api, board: str, push_type: data_type.CommentType, push_cont...
FILE: PyPtt/_api_del_post.py
function del_post (line 17) | def del_post(api, board: str, post_aid: Optional[str] = None, post_index...
FILE: PyPtt/_api_get_board_info.py
function get_board_info (line 15) | def get_board_info(api, board: str, get_post_kind: bool, call_by_others:...
FILE: PyPtt/_api_get_board_list.py
function get_board_list (line 12) | def get_board_list(api) -> list:
FILE: PyPtt/_api_get_bottom_post_list.py
function get_bottom_post_list (line 12) | def get_bottom_post_list(api, board):
FILE: PyPtt/_api_get_favourite_board.py
function get_favourite_board (line 10) | def get_favourite_board(api) -> list:
FILE: PyPtt/_api_get_newest_index.py
function _get_newest_index (line 17) | def _get_newest_index(api) -> int:
function get_newest_index (line 56) | def get_newest_index(api, index_type: data_type.NewIndex, board: Optiona...
FILE: PyPtt/_api_get_post.py
function get_post (line 23) | def get_post(api, board: str, aid: Optional[str] = None, index: Optional...
function _get_post (line 94) | def _get_post(api, board: str, post_aid: Optional[str] = None, post_inde...
FILE: PyPtt/_api_get_post_index.py
function get_post_index (line 10) | def get_post_index(api, board: str, aid: str) -> int:
FILE: PyPtt/_api_get_time.py
function get_time (line 14) | def get_time(api) -> str:
FILE: PyPtt/_api_get_user.py
function get_user (line 20) | def get_user(api, ptt_id: str) -> Dict:
FILE: PyPtt/_api_give_money.py
function give_money (line 11) | def give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag...
FILE: PyPtt/_api_has_new_mail.py
function has_new_mail (line 10) | def has_new_mail(api) -> int:
FILE: PyPtt/_api_loginout.py
function logout (line 14) | def logout(api) -> None:
function login (line 48) | def login(api, ptt_id: str, ptt_pw: str, kick_other_session: bool):
FILE: PyPtt/_api_mail.py
function mail (line 20) | def mail(api,
function get_mail (line 119) | def get_mail(api, index: int, search_type: Optional[data_type.SearchType...
function del_mail (line 272) | def del_mail(api, index) -> None:
FILE: PyPtt/_api_mark_post.py
function mark_post (line 13) | def mark_post(api, mark_type: int, board: str, post_aid: str, post_index...
FILE: PyPtt/_api_post.py
function fast_post_step0 (line 13) | def fast_post_step0(api, board: str, title: str, content: str, post_type...
function fast_post_step1 (line 41) | def fast_post_step1(api: object, sign_file) -> None:
function fast_post (line 57) | def fast_post(
function post (line 95) | def post(api, board: str, title: str, content: str, title_index: int, si...
FILE: PyPtt/_api_reply_post.py
function reply_post (line 12) | def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: st...
FILE: PyPtt/_api_search_user.py
function search_user (line 11) | def search_user(api, ptt_id: str, min_page: int, max_page: int) -> list:
FILE: PyPtt/_api_set_board_title.py
function set_board_title (line 10) | def set_board_title(api, board: str, new_title: str) -> None:
FILE: PyPtt/_api_util.py
function get_content (line 17) | def get_content(api, post_mode: bool = True):
function get_mailbox_capacity (line 185) | def get_mailbox_capacity(api) -> tuple[int, int]:
function parse_query_post (line 215) | def parse_query_post(api, ori_screen):
function get_search_condition_cmd (line 340) | def get_search_condition_cmd(index_type: data_type.NewIndex, search_list...
function goto_board (line 369) | def goto_board(api, board: str, refresh: bool = False, end: bool = False...
function one_thread (line 409) | def one_thread(api):
function check_board (line 416) | def check_board(api, board: str, check_moderator: bool = False) -> Dict:
FILE: PyPtt/check_value.py
function check_type (line 5) | def check_type(value, value_type, name) -> None:
function check_range (line 17) | def check_range(value, min_value, max_value, name) -> None:
function check_index (line 27) | def check_index(name, index, max_value=None) -> None:
function check_index_range (line 39) | def check_index_range(start_name, start_index, end_name, end_index, max_...
FILE: PyPtt/config.py
class Config (line 5) | class Config:
FILE: PyPtt/connect_core.py
class TargetUnit (line 29) | class TargetUnit:
method __init__ (line 30) | def __init__(self, detect_target, log_level: log.LogLevel = None, resp...
method is_match (line 49) | def is_match(self, screen: str) -> bool:
method get_detect_target (line 64) | def get_detect_target(self):
method get_log_level (line 67) | def get_log_level(self):
method get_response (line 70) | def get_response(self, screen: str) -> str:
method is_break (line 75) | def is_break(self) -> bool:
method raise_exception (line 78) | def raise_exception(self):
method is_refresh (line 82) | def is_refresh(self) -> bool:
method is_break_after_send (line 85) | def is_break_after_send(self) -> bool:
method is_secret (line 88) | def is_secret(self) -> bool:
class RecvData (line 92) | class RecvData:
method __init__ (line 93) | def __init__(self):
function websocket_recv_func (line 97) | async def websocket_recv_func(core, recv_data_obj):
function websocket_receiver (line 101) | async def websocket_receiver(core, screen_timeout, recv_data_obj):
class ReceiveDataQueue (line 108) | class ReceiveDataQueue(object):
method __init__ (line 109) | def __init__(self):
method add (line 112) | def add(self, screen):
method get (line 116) | def get(self, last=1):
class API (line 120) | class API(object):
method __init__ (line 121) | def __init__(self, config):
method connect (line 129) | def connect(self) -> None:
method _decode_screen (line 207) | def _decode_screen(self, receive_data_buffer, start_time, target_list,...
method send (line 260) | def send(self, msg: str, target_list: list, screen_timeout: int = 0, r...
method close (line 387) | def close(self):
method get_screen_queue (line 393) | def get_screen_queue(self) -> list:
FILE: PyPtt/data_type.py
class Language (line 7) | class Language:
class ConnectMode (line 12) | class ConnectMode(AutoStrEnum):
class SearchType (line 17) | class SearchType(AutoStrEnum):
class ReplyTo (line 33) | class ReplyTo(AutoStrEnum):
class CommentType (line 41) | class CommentType(AutoStrEnum):
class UserField (line 47) | class UserField(AutoStrEnum):
class CommentField (line 63) | class CommentField(AutoStrEnum):
class PostStatus (line 71) | class PostStatus(AutoStrEnum):
class PostField (line 78) | class PostField(AutoStrEnum):
class Cursor (line 109) | class Cursor:
class NewIndex (line 116) | class NewIndex(AutoStrEnum):
class HOST (line 125) | class HOST(AutoStrEnum):
class MarkType (line 134) | class MarkType(AutoStrEnum):
class FavouriteBoardField (line 147) | class FavouriteBoardField(AutoStrEnum):
class MailField (line 153) | class MailField(AutoStrEnum):
class BoardField (line 164) | class BoardField(AutoStrEnum):
class Compare (line 190) | class Compare(AutoStrEnum):
class TimedDict (line 197) | class TimedDict:
method __init__ (line 198) | def __init__(self, timeout: int = 0):
method __setitem__ (line 203) | def __setitem__(self, key, value):
method __getitem__ (line 207) | def __getitem__(self, key):
method __contains__ (line 217) | def __contains__(self, key):
method __len__ (line 225) | def __len__(self):
method cleanup (line 229) | def cleanup(self):
FILE: PyPtt/exceptions.py
class Error (line 5) | class Error(Exception):
class UnknownError (line 9) | class UnknownError(Error):
method __init__ (line 10) | def __init__(self, message):
method __str__ (line 13) | def __str__(self):
class RequireLogin (line 17) | class RequireLogin(Error):
method __init__ (line 18) | def __init__(self, message):
method __str__ (line 21) | def __str__(self):
class NoPermission (line 25) | class NoPermission(Error):
method __init__ (line 26) | def __init__(self, message):
method __str__ (line 29) | def __str__(self):
class LoginError (line 33) | class LoginError(Error):
method __init__ (line 34) | def __init__(self):
method __str__ (line 37) | def __str__(self):
class NoFastComment (line 41) | class NoFastComment(Error):
method __init__ (line 42) | def __init__(self):
method __str__ (line 45) | def __str__(self):
class NoSuchUser (line 49) | class NoSuchUser(Error):
method __init__ (line 50) | def __init__(self, user):
method __str__ (line 53) | def __str__(self):
class NoSuchMail (line 57) | class NoSuchMail(Error):
method __init__ (line 58) | def __init__(self):
method __str__ (line 61) | def __str__(self):
class NoMoney (line 81) | class NoMoney(Error):
method __init__ (line 82) | def __init__(self):
method __str__ (line 85) | def __str__(self):
class NoSuchBoard (line 89) | class NoSuchBoard(Error):
method __init__ (line 90) | def __init__(self, config, board):
method __str__ (line 107) | def __str__(self):
class ConnectionClosed (line 111) | class ConnectionClosed(Error):
method __init__ (line 112) | def __init__(self):
method __str__ (line 115) | def __str__(self):
class UnregisteredUser (line 119) | class UnregisteredUser(Error):
method __init__ (line 120) | def __init__(self, api_name):
method __str__ (line 123) | def __str__(self):
class MultiThreadOperated (line 127) | class MultiThreadOperated(Error):
method __init__ (line 128) | def __init__(self):
method __str__ (line 131) | def __str__(self):
class WrongIDorPassword (line 135) | class WrongIDorPassword(Error):
method __init__ (line 136) | def __init__(self):
method __str__ (line 139) | def __str__(self):
class WrongPassword (line 143) | class WrongPassword(Error):
method __init__ (line 144) | def __init__(self):
method __str__ (line 147) | def __str__(self):
class LoginTooOften (line 151) | class LoginTooOften(Error):
method __init__ (line 152) | def __init__(self):
method __str__ (line 155) | def __str__(self):
class UseTooManyResources (line 159) | class UseTooManyResources(Error):
method __init__ (line 160) | def __init__(self):
method __str__ (line 163) | def __str__(self):
class HostNotSupport (line 167) | class HostNotSupport(Error):
method __init__ (line 168) | def __init__(self, api):
method __str__ (line 171) | def __str__(self):
class CantComment (line 175) | class CantComment(Error):
method __init__ (line 176) | def __init__(self):
method __str__ (line 179) | def __str__(self):
class CantResponse (line 183) | class CantResponse(Error):
method __init__ (line 184) | def __init__(self):
method __str__ (line 187) | def __str__(self):
class NeedModeratorPermission (line 191) | class NeedModeratorPermission(Error):
method __init__ (line 192) | def __init__(self, board):
method __str__ (line 195) | def __str__(self):
class ConnectError (line 199) | class ConnectError(Error):
method __init__ (line 200) | def __init__(self, config):
method __str__ (line 203) | def __str__(self):
class NoSuchPost (line 207) | class NoSuchPost(Error):
method __init__ (line 208) | def __init__(self, board, aid):
method __str__ (line 214) | def __str__(self):
class CanNotUseSearchPostCode (line 218) | class CanNotUseSearchPostCode(Error):
method __init__ (line 223) | def __init__(self):
method __str__ (line 226) | def __str__(self):
class UserHasPreviouslyBeenBanned (line 230) | class UserHasPreviouslyBeenBanned(Error):
method __init__ (line 231) | def __init__(self):
method __str__ (line 234) | def __str__(self):
class MailboxFull (line 238) | class MailboxFull(Error):
method __init__ (line 239) | def __init__(self):
method __str__ (line 242) | def __str__(self):
class NoSearchResult (line 246) | class NoSearchResult(Error):
method __init__ (line 247) | def __init__(self):
method __str__ (line 250) | def __str__(self):
class OnlySecureConnection (line 256) | class OnlySecureConnection(Error):
method __init__ (line 257) | def __init__(self):
method __str__ (line 260) | def __str__(self):
class SetContactMailFirst (line 264) | class SetContactMailFirst(Error):
method __init__ (line 265) | def __init__(self):
method __str__ (line 268) | def __str__(self):
class ResetYourContactEmail (line 272) | class ResetYourContactEmail(Error):
method __init__ (line 273) | def __init__(self):
method __str__ (line 276) | def __str__(self):
FILE: PyPtt/i18n.py
function replace (line 22) | def replace(string, *args):
function init (line 29) | def init(locale: str, cache: bool = False) -> None:
FILE: PyPtt/lib_util.py
function get_file_name (line 19) | def get_file_name(path_str: str) -> str:
function get_current_func_name (line 25) | def get_current_func_name() -> str:
function findnth (line 29) | def findnth(haystack, needle, n) -> int:
function get_random_str (line 36) | def get_random_str(length) -> str:
function get_aid_from_url (line 45) | def get_aid_from_url(url: str) -> Tuple[str, str]:
function sync_version (line 88) | def sync_version() -> Tuple[data_type.Compare, str]:
function uniform_new_line (line 138) | def uniform_new_line(text: str) -> str:
function check_aid (line 149) | def check_aid(aid: str) -> str:
FILE: PyPtt/log.py
class LogLv (line 5) | class LogLv:
method __init__ (line 8) | def __init__(self, level):
method level (line 12) | def level(self):
method __eq__ (line 15) | def __eq__(self, other):
class LogLevel (line 26) | class LogLevel:
function _combine_msg (line 41) | def _combine_msg(*args) -> str:
class Logger (line 61) | class Logger:
method __init__ (line 64) | def __init__(self, name: str, level: int = logging.NOTSET, logger_call...
method info (line 79) | def info(self, *args):
method debug (line 89) | def debug(self, *args):
function init (line 103) | def init(log_level: LogLv, name: Optional[str] = None, logger_callback: ...
FILE: PyPtt/screens.py
class Target (line 11) | class Target:
function show (line 165) | def show(config, screen_queue, function_name=None):
class VT100Parser (line 198) | class VT100Parser:
method _h (line 199) | def _h(self):
method _2j (line 203) | def _2j(self):
method _move (line 207) | def _move(self, x, y):
method _newline (line 211) | def _newline(self):
method _k (line 215) | def _k(self):
method __init__ (line 221) | def __init__(self, bytes_data, encoding):
FILE: PyPtt/service.py
class Service (line 11) | class Service:
method __init__ (line 13) | def __init__(self, pyptt_init_config: Optional[dict] = None):
method _run (line 91) | def _run(self):
method _get_call_id (line 122) | def _get_call_id(self):
method call (line 131) | def call(self, api: str, args: Optional[dict] = None):
method close (line 163) | def close(self):
FILE: scripts/lang.py
function add_lang (line 13) | def add_lang():
function check_lang (line 28) | def check_lang():
FILE: scripts/package_script.py
function get_next_version (line 6) | def get_next_version():
FILE: setup.py
function version_automation_script (line 8) | def version_automation_script():
FILE: tests/change_pw.py
function test (line 10) | def test(ptt_bot: PyPtt.API):
function func (line 14) | def func():
FILE: tests/comment.py
function test (line 10) | def test(ptt_bot):
function func (line 58) | def func():
FILE: tests/get_board_info.py
function test (line 11) | def test(ptt_bot: PyPtt.API):
function func (line 21) | def func():
FILE: tests/get_board_list.py
function test (line 12) | def test(ptt_bot: PyPtt.API):
function func (line 19) | def func():
FILE: tests/get_bottom_post_list.py
function test (line 10) | def test(ptt_bot: PyPtt.API):
function func (line 16) | def func():
FILE: tests/get_favourite_boards.py
function test (line 10) | def test(ptt_bot: PyPtt.API):
function func (line 17) | def func():
FILE: tests/get_mail.py
function test (line 11) | def test(ptt_bot: PyPtt.API):
function func (line 20) | def func():
FILE: tests/get_newest_index.py
function test_board_index (line 11) | def test_board_index(ptt_bot: PyPtt.API):
function test_mail_index (line 42) | def test_mail_index(ptt_bot: PyPtt.API):
function func (line 49) | def func():
FILE: tests/get_post.py
function test_no_condition (line 13) | def test_no_condition(ptt_bot: PyPtt.API):
function get_post_with_condition (line 71) | def get_post_with_condition(ptt_bot: PyPtt.API):
function test (line 144) | def test(ptt_bot: PyPtt.API):
function func (line 154) | def func():
FILE: tests/get_time.py
function test (line 11) | def test(ptt_bot: PyPtt.API):
function func (line 20) | def func():
FILE: tests/get_user.py
function test (line 11) | def test(ptt_bot: PyPtt.API):
function func (line 22) | def func():
FILE: tests/give_p.py
function test (line 10) | def test(ptt_bot: PyPtt.API):
function func (line 14) | def func():
FILE: tests/i18n.py
function test (line 11) | def test():
FILE: tests/init.py
function test (line 9) | def test():
FILE: tests/logger.py
function func (line 8) | def func():
FILE: tests/login_logout.py
function test (line 11) | def test(ptt_bot: PyPtt.API):
function func (line 27) | def func():
FILE: tests/performance.py
function test (line 11) | def test(ptt_bot):
function func (line 28) | def func():
FILE: tests/post.py
function test (line 15) | def test(ptt_bot: PyPtt.API):
function func (line 120) | def func():
FILE: tests/reply.py
function test (line 15) | def test(ptt_bot: PyPtt.API):
function func (line 91) | def func():
FILE: tests/search_user.py
function test (line 11) | def test(ptt_bot: PyPtt.API):
function func (line 22) | def func():
FILE: tests/service.py
function api_test (line 12) | def api_test(thread_id, service):
function test (line 23) | def test():
FILE: tests/util.py
function log_to_file (line 9) | def log_to_file(msg: str):
function get_id_pw (line 14) | def get_id_pw(password_file):
function login (line 28) | def login(ptt_bot: PyPtt.API, kick: bool = True):
function show_data (line 55) | def show_data(data, key: str = None):
function del_all_post (line 60) | def del_all_post(ptt_bot: PyPtt.API):
Condensed preview — 119 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (379K chars).
[
{
"path": ".github/workflows/deploy.yml",
"chars": 1842,
"preview": "# This workflow will install Python dependencies, run tests and lint with a variety of Python versions\n# For more inform"
},
{
"path": ".github/workflows/docs.yml",
"chars": 786,
"preview": "\nname: docs\n\n# run on merge to master or manual trigger\non:\n pull_request:\n types: [ closed ]\n paths:\n - 'do"
},
{
"path": ".github/workflows/test.yml",
"chars": 1921,
"preview": "# This workflow will install Python dependencies, run tests and lint with a single version of Python\n# For more informat"
},
{
"path": ".gitignore",
"chars": 323,
"preview": "__pycache__/\nbuild/\ndist/\n/CrawlBoardResult.txt\n/.pypirc\nPTTLibrary.egg-info/\n*Out.txt\n/Big5Data.txt\nPTTLibrary-*/\n/PTTL"
},
{
"path": "GourceScript.bat",
"chars": 66,
"preview": "@echo off\ncls\n\ngource --seconds-per-day 0.05 --title \"PTT Library\""
},
{
"path": "LICENSE",
"chars": 7652,
"preview": " GNU LESSER GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007"
},
{
"path": "MANIFEST.in",
"chars": 85,
"preview": "include LICENSE\ninclude README.md\n\n# Include the data files\nrecursive-include PyPtt *"
},
{
"path": "PyPtt/PTT.py",
"chars": 29073,
"preview": "from __future__ import annotations\n\nimport functools\nimport threading\nfrom typing import Dict, Tuple, Callable, List, O"
},
{
"path": "PyPtt/__init__.py",
"chars": 195,
"preview": "__version__ = '1.1.2'\n\nfrom .PTT import API\nfrom .data_type import *\nfrom .exceptions import *\nfrom .log import LogLevel"
},
{
"path": "PyPtt/_api_bucket.py",
"chars": 1910,
"preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import excepti"
},
{
"path": "PyPtt/_api_call_status.py",
"chars": 2177,
"preview": "from . import command\nfrom . import connect_core\nfrom . import data_type\nfrom . import exceptions\nfrom . import log\nfrom"
},
{
"path": "PyPtt/_api_change_pw.py",
"chars": 1604,
"preview": "from . import command, _api_util\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfrom . import lo"
},
{
"path": "PyPtt/_api_comment.py",
"chars": 6902,
"preview": "import collections\nimport time\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import co"
},
{
"path": "PyPtt/_api_del_post.py",
"chars": 3279,
"preview": "from __future__ import annotations\n\nfrom typing import Optional\n\nfrom . import _api_util\nfrom . import check_value\nfrom "
},
{
"path": "PyPtt/_api_get_board_info.py",
"chars": 8026,
"preview": "import re\nfrom typing import Dict\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import"
},
{
"path": "PyPtt/_api_get_board_list.py",
"chars": 3098,
"preview": "import progressbar\n\nfrom . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfr"
},
{
"path": "PyPtt/_api_get_bottom_post_list.py",
"chars": 2246,
"preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_ty"
},
{
"path": "PyPtt/_api_get_favourite_board.py",
"chars": 2557,
"preview": "from . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfro"
},
{
"path": "PyPtt/_api_get_newest_index.py",
"chars": 7267,
"preview": "from __future__ import annotations\n\nimport re\nfrom typing import Optional\n\nfrom . import _api_util\nfrom . import check_v"
},
{
"path": "PyPtt/_api_get_post.py",
"chars": 20967,
"preview": "from __future__ import annotations\n\nimport json\nimport re\nimport time\nfrom typing import Dict, Optional\n\nfrom AutoStrEnu"
},
{
"path": "PyPtt/_api_get_post_index.py",
"chars": 2186,
"preview": "from . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . import i18n\nfro"
},
{
"path": "PyPtt/_api_get_time.py",
"chars": 1084,
"preview": "import re\n\nfrom . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import exceptions\nfrom . impo"
},
{
"path": "PyPtt/_api_get_user.py",
"chars": 4340,
"preview": "import json\nimport re\nfrom typing import Dict\n\nfrom AutoStrEnum import AutoJsonEncoder\n\nfrom . import _api_util\nfrom . i"
},
{
"path": "PyPtt/_api_give_money.py",
"chars": 3812,
"preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import excepti"
},
{
"path": "PyPtt/_api_has_new_mail.py",
"chars": 2087,
"preview": "import re\n\nfrom . import _api_util\nfrom . import command\nfrom . import connect_core\nfrom . import log\nfrom . import scre"
},
{
"path": "PyPtt/_api_loginout.py",
"chars": 7423,
"preview": "import re\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . imp"
},
{
"path": "PyPtt/_api_mail.py",
"chars": 8912,
"preview": "from __future__ import annotations\n\nimport re\nfrom typing import Dict, Optional\n\nfrom . import _api_util\nfrom . import c"
},
{
"path": "PyPtt/_api_mark_post.py",
"chars": 4419,
"preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_ty"
},
{
"path": "PyPtt/_api_post.py",
"chars": 5220,
"preview": "from __future__ import annotations\n\nfrom . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . impor"
},
{
"path": "PyPtt/_api_reply_post.py",
"chars": 3510,
"preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import data_ty"
},
{
"path": "PyPtt/_api_search_user.py",
"chars": 2812,
"preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import excepti"
},
{
"path": "PyPtt/_api_set_board_title.py",
"chars": 1259,
"preview": "from . import _api_util\nfrom . import check_value\nfrom . import command\nfrom . import connect_core\nfrom . import excepti"
},
{
"path": "PyPtt/_api_util.py",
"chars": 15142,
"preview": "from __future__ import annotations\n\nimport functools\nimport re\nimport threading\nfrom typing import Dict, Optional\n\nfrom "
},
{
"path": "PyPtt/check_value.py",
"chars": 2311,
"preview": "from . import i18n\nfrom . import log\n\n\ndef check_type(value, value_type, name) -> None:\n if not isinstance(value, val"
},
{
"path": "PyPtt/command.py",
"chars": 515,
"preview": "# http://www.physics.udel.edu/~watson/scen103/ascii.html\nenter = '\\r'\ntab = '\\t'\nctrl_c = '\\x03'\nctrl_d = '\\x04'\nctrl_e "
},
{
"path": "PyPtt/config.py",
"chars": 740,
"preview": "from . import data_type\nfrom . import log\n\n\nclass Config:\n # retry_wait_time 秒後重新連線\n retry_wait_time = 3\n\n # Sc"
},
{
"path": "PyPtt/connect_core.py",
"chars": 13838,
"preview": "from __future__ import annotations\n\nimport asyncio\nimport ssl\nimport telnetlib\nimport threading\nimport time\nimport trace"
},
{
"path": "PyPtt/data_type.py",
"chars": 4874,
"preview": "import time\nfrom enum import auto\n\nfrom AutoStrEnum import AutoStrEnum\n\n\nclass Language:\n MANDARIN = 'zh_TW'\n ENGL"
},
{
"path": "PyPtt/exceptions.py",
"chars": 5523,
"preview": "from . import data_type\nfrom . import i18n\n\n\nclass Error(Exception):\n pass\n\n\nclass UnknownError(Error):\n def __ini"
},
{
"path": "PyPtt/i18n.py",
"chars": 1341,
"preview": "import os\nimport random\n\nimport yaml\n\nfrom . import __version__\nfrom . import data_type\n\nlocale_pool = {\n data_type.L"
},
{
"path": "PyPtt/lang/en_US.yaml",
"chars": 4094,
"preview": "PTT: PTT\nPTT2: PTT2\nactive: Active\nauthor: Author\nboard: Board\ncan_not_use_search_post_code_f: This status can not use t"
},
{
"path": "PyPtt/lang/zh_TW.yaml",
"chars": 2691,
"preview": "PTT: 批踢踢\nPTT2: 批踢踢兔\nactive: 啟動\nauthor: 作者\nboard: 看板\ncan_not_use_search_post_code_f: 此狀態下無法使用搜尋文章代碼(AID)功能\ncatch_bottom_p"
},
{
"path": "PyPtt/lib_util.py",
"chars": 4488,
"preview": "import functools\nimport os\nimport random\nimport re\nimport string\nimport time\nimport traceback\nfrom typing import Tuple\n\n"
},
{
"path": "PyPtt/log.py",
"chars": 2895,
"preview": "import logging\nfrom typing import Optional\n\n\nclass LogLv:\n _level: int\n\n def __init__(self, level):\n self._"
},
{
"path": "PyPtt/screens.py",
"chars": 54238,
"preview": "import re\nimport sys\n\nfrom uao import register_uao\n\nfrom . import log\n\nregister_uao()\n\n\nclass Target:\n MainMenu = [\n "
},
{
"path": "PyPtt/service.py",
"chars": 4496,
"preview": "import threading\nimport time\nimport uuid\nfrom typing import Optional\n\nfrom . import PTT\nfrom . import check_value\nfrom ."
},
{
"path": "README.md",
"chars": 1551,
"preview": "\n# PyPtt\n[![Package Vers"
},
{
"path": "docs/CNAME",
"chars": 8,
"preview": "pyptt.cc"
},
{
"path": "docs/Makefile",
"chars": 634,
"preview": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the "
},
{
"path": "docs/api/bucket.rst",
"chars": 94,
"preview": "bucket\n==========\n\n.. _api-bucket:\n\n.. automodule:: PyPtt.API\n :members: bucket\n :noindex:"
},
{
"path": "docs/api/change_pw.rst",
"chars": 87,
"preview": "change_pw\n==============\n\n.. automodule:: PyPtt.API\n :members: change_pw\n :noindex:"
},
{
"path": "docs/api/comment.rst",
"chars": 97,
"preview": "comment\n==========\n\n.. _api-comment:\n\n.. automodule:: PyPtt.API\n :members: comment\n :noindex:"
},
{
"path": "docs/api/del_mail.rst",
"chars": 102,
"preview": "del_mail\n============\n\n.. _api-del-mail:\n\n.. automodule:: PyPtt.API\n :members: del_mail\n :noindex:"
},
{
"path": "docs/api/del_post.rst",
"chars": 85,
"preview": "del_post\n==============\n\n.. automodule:: PyPtt.API\n :members: del_post\n :noindex:"
},
{
"path": "docs/api/get_aid_from_url.rst",
"chars": 108,
"preview": "get_aid_from_url\n=====================\n\n.. automodule:: PyPtt.API\n :members: get_aid_from_url\n :noindex:"
},
{
"path": "docs/api/get_all_boards.rst",
"chars": 100,
"preview": "get_all_boards\n=================\n\n.. automodule:: PyPtt.API\n :members: get_all_boards\n :noindex:"
},
{
"path": "docs/api/get_board_info.rst",
"chars": 125,
"preview": "get_board_info\n=================\n\n.. _api-get-board-info:\n\n.. automodule:: PyPtt.API\n :members: get_board_info\n :noi"
},
{
"path": "docs/api/get_bottom_post_list.rst",
"chars": 117,
"preview": "get_bottom_post_list\n======================\n\n.. automodule:: PyPtt.API\n :members: get_bottom_post_list\n :noindex:"
},
{
"path": "docs/api/get_favourite_boards.rst",
"chars": 152,
"preview": "get_favourite_boards\n==========================\n\n.. _api-get-favourite-boards:\n\n.. automodule:: PyPtt.API\n :members: g"
},
{
"path": "docs/api/get_mail.rst",
"chars": 102,
"preview": "get_mail\n============\n\n.. _api-get-mail:\n\n.. automodule:: PyPtt.API\n :members: get_mail\n :noindex:"
},
{
"path": "docs/api/get_newest_index.rst",
"chars": 131,
"preview": "get_newest_index\n=================\n\n.. _api-get-newest-index:\n\n.. automodule:: PyPtt.API\n :members: get_newest_index\n "
},
{
"path": "docs/api/get_post.rst",
"chars": 100,
"preview": "get_post\n==========\n\n.. _api-get-post:\n\n.. automodule:: PyPtt.API\n :members: get_post\n :noindex:"
},
{
"path": "docs/api/get_time.rst",
"chars": 100,
"preview": "get_time\n==========\n\n.. _api-get-time:\n\n.. automodule:: PyPtt.API\n :members: get_time\n :noindex:"
},
{
"path": "docs/api/get_user.rst",
"chars": 100,
"preview": "get_user\n==========\n\n.. _api-get-user:\n\n.. automodule:: PyPtt.API\n :members: get_user\n :noindex:"
},
{
"path": "docs/api/give_money.rst",
"chars": 109,
"preview": "give_money\n=============\n\n.. _api-give-money:\n\n.. automodule:: PyPtt.API\n :members: give_money\n :noindex:"
},
{
"path": "docs/api/index.rst",
"chars": 654,
"preview": "APIs\n=============\n| 這是 PyPtt 的 API 文件。\n| 我們在這裡介紹 PyPtt 目前所有支援 PTT, PTT2 的功能。\n\n基本功能\n----------------\n.. toctree::\n\n in"
},
{
"path": "docs/api/init.rst",
"chars": 76,
"preview": "init\n=======\n\n.. _api-init:\n\n.. automodule:: PyPtt.API\n :members: __init__"
},
{
"path": "docs/api/login_logout.rst",
"chars": 120,
"preview": "login, logout\n================\n\n.. _api-login-logout:\n\n.. automodule:: PyPtt.API\n :members: login, logout\n :noindex:"
},
{
"path": "docs/api/mail.rst",
"chars": 91,
"preview": "mail\n=============\n\n.. _api-mail:\n\n.. automodule:: PyPtt.API\n :members: mail\n :noindex:"
},
{
"path": "docs/api/mark_post.rst",
"chars": 108,
"preview": "mark_post\n===============\n\n.. _api-mark-post:\n\n.. automodule:: PyPtt.API\n :members: mark_post\n :noindex:"
},
{
"path": "docs/api/post.rst",
"chars": 88,
"preview": "post\n==========\n\n.. _api-post:\n\n.. automodule:: PyPtt.API\n :members: post\n :noindex:"
},
{
"path": "docs/api/reply_post.rst",
"chars": 106,
"preview": "reply_post\n==========\n\n.. _api-reply-post:\n\n.. automodule:: PyPtt.API\n :members: reply_post\n :noindex:"
},
{
"path": "docs/api/search_user.rst",
"chars": 115,
"preview": "search_user\n================\n\n.. _api-search-user:\n\n.. automodule:: PyPtt.API\n :members: search_user\n :noindex:"
},
{
"path": "docs/api/set_board_title.rst",
"chars": 132,
"preview": "set_board_title\n=====================\n\n.. _api-set-board-title:\n\n.. automodule:: PyPtt.API\n :members: set_board_title\n"
},
{
"path": "docs/changelog.rst",
"chars": 462,
"preview": "更新日誌\n====================\n| 這裡寫著 PyPtt 的故事。\n|\n| 2022.12.20 PyPtt 1.0.3,logger 改採用以 logging_ 為基底。\n\n.. _logging: https://d"
},
{
"path": "docs/conf.py",
"chars": 1552,
"preview": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see t"
},
{
"path": "docs/dev.rst",
"chars": 1302,
"preview": "Development\n================\n如果你想參與開發,請參考以下須知:\n\n開發環境\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n我們建議您使用 virt"
},
{
"path": "docs/docker.rst",
"chars": 2708,
"preview": "Docker Image\n=================\n\n.. image:: https://img.shields.io/docker/v/codingman000/pyptt/latest\n :target: https:/"
},
{
"path": "docs/examples.rst",
"chars": 3228,
"preview": "使用範例\n=============\n| 這裡記錄了各種實際使用的範例 ☺️\n\n保持登入\n--------\n這裡示範了如何保持登入\n\n.. code-block:: python\n\n import PyPtt\n\n def log"
},
{
"path": "docs/exceptions.rst",
"chars": 2518,
"preview": "例外\n========\n| 這裡介紹 PyPtt 的例外。\n| 可以用 try...except... 來處理。\n\n| 例外的種類\n\n.. py:exception:: PyPtt.exceptions.RequireLogin\n :"
},
{
"path": "docs/faq.rst",
"chars": 1947,
"preview": "FAQ\n==========\n這裡搜集了一些常見問題的解答,如果你有任何問題,請先看看這裡。\n\nQ: 我該如何使用 PyPtt?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
},
{
"path": "docs/index.rst",
"chars": 2561,
"preview": "PyPtt\n====================\n\n.. image:: _static/logo_cover.png\n :alt: PyPtt: PTT bot library for Python\n :align: ce"
},
{
"path": "docs/install.rst",
"chars": 1597,
"preview": "安裝 PyPtt\n===================\n\nPython 版本\n--------------\n| 推薦使用 CPython_ 3.8+。\n\n.. _CPython: https://www.python.org/\n\n相依套件"
},
{
"path": "docs/make.bat",
"chars": 800,
"preview": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sp"
},
{
"path": "docs/requirements.txt",
"chars": 67,
"preview": "PyPtt\nsphinx\nsphinx-copybutton\npygments==2.15.0\nFuro\nsphinx-sitemap"
},
{
"path": "docs/roadmap.rst",
"chars": 735,
"preview": "開發\n=============\n| 這裡列了一些我們正在開發的功能,如果你有任何建議,歡迎找我們聊聊。\n| 或者你也可以直接在 github 上開 issue,我們會盡快回覆。\n\n| 當然如果你有興趣參與開發,也歡迎你加入我們,我們會盡快"
},
{
"path": "docs/robots.txt",
"chars": 52,
"preview": "User-agent: *\n\nSitemap: https://pyptt.cc/sitemap.xml"
},
{
"path": "docs/service.rst",
"chars": 95,
"preview": "Service\n===========\n\n\n.. automodule:: PyPtt.Service\n :members: __init__\n :undoc-members:\n"
},
{
"path": "docs/type.rst",
"chars": 6714,
"preview": "參數型態\n===========\n這裡介紹 PyPtt 的參數型態\n\n.. _host:\n\nHOST\n-----------\n* 連線的 PTT 伺服器。\n\n.. py:attribute:: PyPtt.HOST.PTT1\n\n 批踢"
},
{
"path": "make_doc.sh",
"chars": 38,
"preview": "make -C docs/ clean\nmake -C docs/ html"
},
{
"path": "requirements.txt",
"chars": 63,
"preview": "progressbar2\nwebsockets\nuao\nrequests==2.31.0\nAutoStrEnum\nPyYAML"
},
{
"path": "scripts/lang.py",
"chars": 2459,
"preview": "import json\nimport os\nimport sys\nfrom collections import defaultdict\n\nimport yaml\n\nsys.path.append(os.getcwd())\n\nimport "
},
{
"path": "scripts/package_script.py",
"chars": 2791,
"preview": "import os\nimport subprocess\nimport time\n\n\ndef get_next_version():\n is_merged = os.environ.get('GITHUB_EVENT_NAME') =="
},
{
"path": "setup.py",
"chars": 4441,
"preview": "import os\nimport subprocess\nimport time\n\nfrom setuptools import setup\n\n\ndef version_automation_script():\n is_merged ="
},
{
"path": "tests/change_pw.py",
"chars": 539,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n"
},
{
"path": "tests/comment.py",
"chars": 1919,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot):\n if ptt_"
},
{
"path": "tests/config.py",
"chars": 142,
"preview": "import os\n\n#\nPTT1_ID = os.environ['PTT1_ID']\nPTT1_PW = os.environ['PTT1_PW']\n\nPTT2_ID = os.environ['PTT2_ID']\nPTT2_PW = "
},
{
"path": "tests/exceptions.py",
"chars": 207,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\nimport PyPtt\n\nif __name__ == '__main__':\n try:\n raise PyPtt"
},
{
"path": "tests/get_board_info.py",
"chars": 698,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test"
},
{
"path": "tests/get_board_list.py",
"chars": 686,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport json\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot:"
},
{
"path": "tests/get_bottom_post_list.py",
"chars": 570,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n"
},
{
"path": "tests/get_favourite_boards.py",
"chars": 583,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n"
},
{
"path": "tests/get_mail.py",
"chars": 724,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test"
},
{
"path": "tests/get_newest_index.py",
"chars": 2022,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test"
},
{
"path": "tests/get_post.py",
"chars": 4787,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport json\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import ut"
},
{
"path": "tests/get_time.py",
"chars": 672,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test"
},
{
"path": "tests/get_user.py",
"chars": 708,
"preview": "import json\nimport os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: "
},
{
"path": "tests/give_p.py",
"chars": 542,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: PyPtt.API):\n"
},
{
"path": "tests/i18n.py",
"chars": 360,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import i18n\nfrom PyPtt import log\n\n\ndef test"
},
{
"path": "tests/init.py",
"chars": 1602,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\n\n\ndef test():\n print('=== default ===')\n PyPtt.AP"
},
{
"path": "tests/logger.py",
"chars": 529,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\nfrom PyPtt import log\n\n\ndef func():\n logger = log.init(log.INFO)\n\n"
},
{
"path": "tests/login_logout.py",
"chars": 727,
"preview": "import os\nimport sys\nimport time\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot: "
},
{
"path": "tests/performance.py",
"chars": 667,
"preview": "import os\nimport sys\nimport time\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom tests import util\n\n\ndef test(ptt_bot):"
},
{
"path": "tests/post.py",
"chars": 3558,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport time\n\nimport PyPtt\n\nfrom PyPtt import PostField\nfrom tests im"
},
{
"path": "tests/reply.py",
"chars": 2666,
"preview": "import os\nimport sys\nimport time\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom PyPtt import Pos"
},
{
"path": "tests/search_user.py",
"chars": 690,
"preview": "import os\nimport sys\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import log\nfrom tests import util\n\n\ndef test"
},
{
"path": "tests/service.py",
"chars": 1167,
"preview": "import os\nimport sys\nimport threading\n\nsys.path.append(os.getcwd())\n\nimport PyPtt\nfrom PyPtt import Service\nfrom tests i"
},
{
"path": "tests/util.py",
"chars": 1842,
"preview": "import json\n\nimport PyPtt\nfrom PyPtt import PostField\nfrom PyPtt import log\nfrom . import config\n\n\ndef log_to_file(msg: "
},
{
"path": "upload.sh",
"chars": 355,
"preview": "#!/bin/sh\n\necho PyPtt uploader v 1.0.2\n\nrm -r dist build\npython3 setup.py sdist bdist_wheel --universal\n\ncase $1 in\nrele"
}
]
About this extraction
This page contains the full source code of the PttCodingMan/PyPtt GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 119 files (328.8 KB), approximately 106.0k tokens, and a symbol index with 299 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.