PyPtt 2.2.7__tar.gz → 2.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyptt-2.2.7 → pyptt-2.3.1}/PKG-INFO +1 -1
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/PTT.py +3 -71
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/__init__.py +1 -1
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_board_info.py +17 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_newest_index.py +4 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_post.py +11 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_post_index.py +2 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_user.py +3 -1
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_mail.py +2 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_reply_post.py +5 -2
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_util.py +4 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/connect_core.py +43 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/data_type.py +0 -21
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/exceptions.py +0 -8
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/lang_en_US.py +0 -3
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/lang_zh_TW.py +0 -3
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt.egg-info/PKG-INFO +1 -1
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt.egg-info/SOURCES.txt +3 -2
- pyptt-2.3.1/tests/test_backup_param.py +41 -0
- pyptt-2.3.1/tests/test_connect_core_timeout.py +113 -0
- pyptt-2.3.1/tests/test_half_dead_connection.py +314 -0
- pyptt-2.2.7/PyPtt/_api_lottery.py +0 -164
- pyptt-2.2.7/tests/test_lottery.py +0 -92
- {pyptt-2.2.7 → pyptt-2.3.1}/LICENSE +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_bucket.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_call_status.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_change_pw.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_comment.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_del_post.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_board_list.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_bottom_post_list.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_favourite_board.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_post_list.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_time.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_get_waterball.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_give_money.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_has_new_mail.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_loginout.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_mark_post.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_post.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_search_user.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_set_board_title.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_api_set_signature_file.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_uao/LICENSE +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_uao/__init__.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_uao/b2u.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/_uao/u2b.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/api_server.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/check_value.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/command.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/config.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/i18n.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/lib_util.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/log.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/screens.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/service.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt/ssl_config.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt.egg-info/dependency_links.txt +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt.egg-info/requires.txt +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/PyPtt.egg-info/top_level.txt +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/README.md +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/pyproject.toml +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/setup.cfg +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/setup.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_bucket.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_change_pw.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_comment.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_del_post.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_del_post_unit.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_exceptions.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_fast_post.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_board_info.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_board_list.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_bottom_post_list.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_content_formula.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_favourite_boards.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_mail.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_newest_index.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_post.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_post_list.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_post_list_parser.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_post_parser.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_time.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_user.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_get_waterball.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_give_money.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_i18n.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_incremental_parity.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_init.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_logger.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_login_logout.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_mail_send_and_del.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_mark_post.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_parse_query_post.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_post.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_post_anonymous.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_pyte_parity.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_reply_post.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_search_user.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_service.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_set_board_title.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_set_signature_file.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_uao_codec.py +0 -0
- {pyptt-2.2.7 → pyptt-2.3.1}/tests/test_vt100_parser.py +0 -0
|
@@ -21,7 +21,6 @@ from . import _api_get_user
|
|
|
21
21
|
from . import _api_give_money
|
|
22
22
|
from . import _api_loginout
|
|
23
23
|
from . import _api_get_waterball
|
|
24
|
-
from . import _api_lottery
|
|
25
24
|
from . import _api_mail
|
|
26
25
|
from . import _api_mark_post
|
|
27
26
|
from . import _api_post
|
|
@@ -603,74 +602,6 @@ class API:
|
|
|
603
602
|
|
|
604
603
|
_api_give_money.give_money(self, ptt_id, money, red_bag_title, red_bag_content)
|
|
605
604
|
|
|
606
|
-
def get_lottery(self, board: str) -> Dict:
|
|
607
|
-
|
|
608
|
-
"""
|
|
609
|
-
取得看板樂透(彩券)目前的開獎資訊,對應看板內按 `f` 鍵的功能。
|
|
610
|
-
|
|
611
|
-
Args:
|
|
612
|
-
board (str): 看板名稱。
|
|
613
|
-
|
|
614
|
-
Returns:
|
|
615
|
-
Dict,樂透資訊,詳見 :ref:`lottery-field`。
|
|
616
|
-
|
|
617
|
-
Raises:
|
|
618
|
-
RequireLogin: 需要登入。
|
|
619
|
-
NoSuchBoard: 看板不存在。
|
|
620
|
-
NoSuchLottery: 該看板目前並沒有舉辦樂透,或樂透已封盤/開獎。
|
|
621
|
-
|
|
622
|
-
範例::
|
|
623
|
-
|
|
624
|
-
import PyPtt
|
|
625
|
-
|
|
626
|
-
ptt_bot = PyPtt.API()
|
|
627
|
-
try:
|
|
628
|
-
# .. login ..
|
|
629
|
-
lottery = ptt_bot.get_lottery(board='Test')
|
|
630
|
-
# .. do something ..
|
|
631
|
-
finally:
|
|
632
|
-
ptt_bot.logout()
|
|
633
|
-
|
|
634
|
-
"""
|
|
635
|
-
|
|
636
|
-
return _api_lottery.get_lottery(self, board)
|
|
637
|
-
|
|
638
|
-
def bet_lottery(self, board: str, item: int = 1, amount: int = 1) -> Dict:
|
|
639
|
-
|
|
640
|
-
"""
|
|
641
|
-
在看板下注樂透(彩券),對應看板內按 `f` 鍵後購買彩券的功能。
|
|
642
|
-
|
|
643
|
-
Args:
|
|
644
|
-
board (str): 看板名稱。
|
|
645
|
-
item (int): 要購買的樂透選項編號,範圍 1~8(依看板實際選項數而定)。
|
|
646
|
-
amount (int): 購買張數,範圍 1~9999,並受限於帳戶內的 Ptt 幣餘額。
|
|
647
|
-
|
|
648
|
-
Returns:
|
|
649
|
-
Dict,下注結果,詳見 :ref:`lottery-bet-field`。
|
|
650
|
-
|
|
651
|
-
Raises:
|
|
652
|
-
RequireLogin: 需要登入。
|
|
653
|
-
UnregisteredUser: 未註冊使用者。
|
|
654
|
-
NoSuchBoard: 看板不存在。
|
|
655
|
-
NoSuchLottery: 該看板目前並沒有舉辦樂透,或樂透已封盤/開獎。
|
|
656
|
-
NoMoney: 餘額不足。
|
|
657
|
-
|
|
658
|
-
範例::
|
|
659
|
-
|
|
660
|
-
import PyPtt
|
|
661
|
-
|
|
662
|
-
ptt_bot = PyPtt.API()
|
|
663
|
-
try:
|
|
664
|
-
# .. login ..
|
|
665
|
-
result = ptt_bot.bet_lottery(board='Test', item=1, amount=10)
|
|
666
|
-
# .. do something ..
|
|
667
|
-
finally:
|
|
668
|
-
ptt_bot.logout()
|
|
669
|
-
|
|
670
|
-
"""
|
|
671
|
-
|
|
672
|
-
return _api_lottery.bet_lottery(self, board, item, amount)
|
|
673
|
-
|
|
674
605
|
def mail(self, ptt_id: str, title: str, content: str, sign_file: [int | str] = 0,
|
|
675
606
|
backup: bool = True) -> None:
|
|
676
607
|
|
|
@@ -734,7 +665,7 @@ class API:
|
|
|
734
665
|
return _api_get_board_list.get_board_list(self)
|
|
735
666
|
|
|
736
667
|
def reply_post(self, reply_to: data_type.ReplyTo, board: str, content: str, sign_file: [str | int] = 0,
|
|
737
|
-
aid: Optional[str] = None, index: int = 0) -> None:
|
|
668
|
+
aid: Optional[str] = None, index: int = 0, backup: bool = True) -> None:
|
|
738
669
|
|
|
739
670
|
"""
|
|
740
671
|
回覆文章。
|
|
@@ -746,6 +677,7 @@ class API:
|
|
|
746
677
|
sign_file (str | int): 編號或隨機簽名檔 (x),預設為 **0** (不選)。
|
|
747
678
|
aid: 文章編號。
|
|
748
679
|
index: 文章編號。
|
|
680
|
+
backup (bool): 回信給作者時是否自存底稿,預設為 True。僅在回覆類型含站內信時有作用。
|
|
749
681
|
|
|
750
682
|
Returns:
|
|
751
683
|
None
|
|
@@ -772,7 +704,7 @@ class API:
|
|
|
772
704
|
參考 :ref:`回覆類型 <reply-to>`、:ref:`取得最新文章編號 <api-get-newest-index>`
|
|
773
705
|
"""
|
|
774
706
|
|
|
775
|
-
_api_reply_post.reply_post(self, reply_to, board, content, sign_file, aid, index)
|
|
707
|
+
_api_reply_post.reply_post(self, reply_to, board, content, sign_file, aid, index, backup)
|
|
776
708
|
|
|
777
709
|
def set_board_title(self, board: str, new_title: str) -> None:
|
|
778
710
|
"""
|
|
@@ -27,6 +27,11 @@ def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -
|
|
|
27
27
|
|
|
28
28
|
_api_util.goto_board(api, board, refresh=True)
|
|
29
29
|
|
|
30
|
+
# last_timeout_was_silent is only meaningful right after a send(), and
|
|
31
|
+
# goto_board()'s last action is one. Nothing below sends until the 'i'
|
|
32
|
+
# further down, so the flag stays valid for the whole parse block: a
|
|
33
|
+
# totally silent timeout means goto_board never redrew and we are about
|
|
34
|
+
# to parse a stale screen -- a half-dead connection, not a missing board.
|
|
30
35
|
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
31
36
|
# print(ori_screen)
|
|
32
37
|
|
|
@@ -41,16 +46,22 @@ def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -
|
|
|
41
46
|
break
|
|
42
47
|
|
|
43
48
|
if online_user_line is None:
|
|
49
|
+
if api.connect_core.last_timeout_was_silent:
|
|
50
|
+
raise exceptions.ConnectionClosed()
|
|
44
51
|
raise exceptions.NoSuchBoard(api.config, board)
|
|
45
52
|
|
|
46
53
|
if '[靜]' in online_user_line:
|
|
47
54
|
online_user = 0
|
|
48
55
|
else:
|
|
49
56
|
if '日 期' not in online_user_line or '作 者' not in online_user_line:
|
|
57
|
+
if api.connect_core.last_timeout_was_silent:
|
|
58
|
+
raise exceptions.ConnectionClosed()
|
|
50
59
|
raise exceptions.NoSuchBoard(api.config, board)
|
|
51
60
|
pattern = re.compile(r'[\d]+')
|
|
52
61
|
r = pattern.search(online_user_line)
|
|
53
62
|
if r is None:
|
|
63
|
+
if api.connect_core.last_timeout_was_silent:
|
|
64
|
+
raise exceptions.ConnectionClosed()
|
|
54
65
|
raise exceptions.NoSuchBoard(api.config, board)
|
|
55
66
|
# 減一是把自己本身拿掉
|
|
56
67
|
online_user = int(r.group(0)) - 1
|
|
@@ -66,6 +77,8 @@ def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -
|
|
|
66
77
|
'i',
|
|
67
78
|
target_list)
|
|
68
79
|
|
|
80
|
+
# Same flag window as above: the 'i' send() right before is the last one
|
|
81
|
+
# until the get_post_kind block, so the flag still describes it here.
|
|
69
82
|
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
70
83
|
# print(ori_screen)
|
|
71
84
|
|
|
@@ -76,10 +89,14 @@ def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -
|
|
|
76
89
|
boardname = r.group(0)[1:-5].strip()
|
|
77
90
|
|
|
78
91
|
if boardname is None:
|
|
92
|
+
if api.connect_core.last_timeout_was_silent:
|
|
93
|
+
raise exceptions.ConnectionClosed()
|
|
79
94
|
raise exceptions.NoSuchBoard(api.config, board)
|
|
80
95
|
logger.debug('看板名稱', boardname, board)
|
|
81
96
|
|
|
82
97
|
if boardname.lower() != board.lower():
|
|
98
|
+
if api.connect_core.last_timeout_was_silent:
|
|
99
|
+
raise exceptions.ConnectionClosed()
|
|
83
100
|
raise exceptions.NoSuchBoard(api.config, board)
|
|
84
101
|
|
|
85
102
|
chinese_des = ''
|
|
@@ -104,6 +104,8 @@ def get_newest_index(api, index_type: data_type.NewIndex, board: Optional[str] =
|
|
|
104
104
|
|
|
105
105
|
index = api.connect_core.send(cmd, target_list)
|
|
106
106
|
if index < 0:
|
|
107
|
+
if api.connect_core.last_timeout_was_silent:
|
|
108
|
+
raise exceptions.ConnectionClosed()
|
|
107
109
|
raise exceptions.NoSuchBoard(api.config, board)
|
|
108
110
|
|
|
109
111
|
if index == 0:
|
|
@@ -127,6 +129,8 @@ def get_newest_index(api, index_type: data_type.NewIndex, board: Optional[str] =
|
|
|
127
129
|
|
|
128
130
|
index = api.connect_core.send(cmd, target_list)
|
|
129
131
|
if index < 0:
|
|
132
|
+
if api.connect_core.last_timeout_was_silent:
|
|
133
|
+
raise exceptions.ConnectionClosed()
|
|
130
134
|
raise exceptions.NoSuchBoard(api.config, board)
|
|
131
135
|
|
|
132
136
|
if index == 0:
|
|
@@ -156,6 +156,17 @@ def _get_post(api, board: str, post_aid: Optional[str] = None, post_index: int =
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
if index < 0 or index == 1:
|
|
159
|
+
# Known gap (intentionally not fixed here): a connection that dies
|
|
160
|
+
# mid-redraw (bytes received, so last_timeout_was_silent is False)
|
|
161
|
+
# leaves a stale listing screen with a cursor line in RDQ, and
|
|
162
|
+
# _parse_deleted_post() will happily parse that into a normal-looking
|
|
163
|
+
# PostStatus.DELETED_BY_AUTHOR dict -- a false "deleted" report that
|
|
164
|
+
# this flag cannot catch. Fixing that needs dropping `index < 0` as a
|
|
165
|
+
# deleted-post signal outright, which first needs verifying what a
|
|
166
|
+
# real deleted post's Q-response looks like. Only the fully-silent
|
|
167
|
+
# (zero bytes received) timeout is handled below.
|
|
168
|
+
if index < 0 and api.connect_core.last_timeout_was_silent:
|
|
169
|
+
raise exceptions.ConnectionClosed()
|
|
159
170
|
return _parse_deleted_post(api, post, board, last_screen)
|
|
160
171
|
|
|
161
172
|
# index == 0: QueryPost screen matched
|
|
@@ -35,6 +35,8 @@ def get_post_index(api, board: str, aid: str) -> int:
|
|
|
35
35
|
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
36
36
|
if index < 0:
|
|
37
37
|
# print(OriScreen)
|
|
38
|
+
if api.connect_core.last_timeout_was_silent:
|
|
39
|
+
raise exceptions.ConnectionClosed()
|
|
38
40
|
raise exceptions.NoSuchBoard(api.config, board)
|
|
39
41
|
|
|
40
42
|
# if index == 5:
|
|
@@ -59,9 +59,11 @@ def get_user(api, ptt_id: str) -> Dict:
|
|
|
59
59
|
]
|
|
60
60
|
|
|
61
61
|
index = api.connect_core.send(cmd, target_list)
|
|
62
|
-
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
63
62
|
if index != 0:
|
|
63
|
+
if api.connect_core.last_timeout_was_silent:
|
|
64
|
+
raise exceptions.ConnectionClosed()
|
|
64
65
|
raise exceptions.NoSuchUser(ptt_id)
|
|
66
|
+
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
65
67
|
# PTT1
|
|
66
68
|
# 《ID暱稱》CodingMan (專業程式 BUG 製造機)《經濟狀況》小康 ($73866)
|
|
67
69
|
# 《登入次數》1118 次 (同天內只計一次) 《有效文章》15 篇 (退:0)
|
|
@@ -34,6 +34,8 @@ def mail(api,
|
|
|
34
34
|
check_value.check_type(ptt_id, str, 'ptt_id')
|
|
35
35
|
check_value.check_type(title, str, 'title')
|
|
36
36
|
check_value.check_type(content, str, 'content')
|
|
37
|
+
# 沒擋型別的話,backup='False' 這種字串是 truthy,會靜靜地存底稿。
|
|
38
|
+
check_value.check_type(backup, bool, 'backup')
|
|
37
39
|
|
|
38
40
|
_api_util.check_user_exist(api, ptt_id)
|
|
39
41
|
|
|
@@ -10,7 +10,7 @@ from . import log
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: str, sign_file, post_aid: str,
|
|
13
|
-
post_index: int) -> None:
|
|
13
|
+
post_index: int, backup: bool = True) -> None:
|
|
14
14
|
_api_util.one_thread(api)
|
|
15
15
|
|
|
16
16
|
if not api._is_login:
|
|
@@ -21,6 +21,8 @@ def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: str, sign_
|
|
|
21
21
|
|
|
22
22
|
check_value.check_type(board, str, 'board')
|
|
23
23
|
check_value.check_type(content, str, 'content')
|
|
24
|
+
# 沒擋型別的話,backup='False' 這種字串是 truthy,會靜靜地存底稿。
|
|
25
|
+
check_value.check_type(backup, bool, 'backup')
|
|
24
26
|
if post_aid is not None:
|
|
25
27
|
check_value.check_type(post_aid, str, 'PostAID')
|
|
26
28
|
|
|
@@ -85,7 +87,8 @@ def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: str, sign_
|
|
|
85
87
|
connect_core.TargetUnit('請問要引用原文嗎', log_level=log.DEBUG, response='Y' + command.enter),
|
|
86
88
|
connect_core.TargetUnit('採用原標題[Y/n]?', log_level=log.DEBUG, response='Y' + command.enter),
|
|
87
89
|
reply_target_unit,
|
|
88
|
-
connect_core.TargetUnit('已順利寄出,是否自存底稿', log_level=log.DEBUG,
|
|
90
|
+
connect_core.TargetUnit('已順利寄出,是否自存底稿', log_level=log.DEBUG,
|
|
91
|
+
response=('Y' if backup else 'N') + command.enter),
|
|
89
92
|
]
|
|
90
93
|
|
|
91
94
|
api.connect_core.send(
|
|
@@ -448,12 +448,16 @@ def goto_board(api, board: str, refresh: bool = False, end: bool = False) -> Non
|
|
|
448
448
|
# 但某些看板會卡在進版動畫中,但沒有顯示任意鍵繼續或互動是動畫,所以當 index == -1 (表示找不到標的 timeout 了)
|
|
449
449
|
# 可以嘗試修改 cmd_list
|
|
450
450
|
index = api.connect_core.send(cmd, target_list, refresh=current_refresh)
|
|
451
|
+
if index == -1 and api.connect_core.last_timeout_was_silent:
|
|
452
|
+
raise exceptions.ConnectionClosed()
|
|
451
453
|
|
|
452
454
|
# index == 4 表示偵測到 MainMenu_Exiting,代表導航到看板失敗
|
|
453
455
|
# 可能是暫時性的問題(閒置太久、連線狀態改變等),重試一次
|
|
454
456
|
if index == 4:
|
|
455
457
|
log.logger.debug('goto_board', board, 'failed, retrying')
|
|
456
458
|
index = api.connect_core.send(cmd, target_list, refresh=True)
|
|
459
|
+
if index == -1 and api.connect_core.last_timeout_was_silent:
|
|
460
|
+
raise exceptions.ConnectionClosed()
|
|
457
461
|
if index == 4:
|
|
458
462
|
raise exceptions.NoSuchBoard(api.config, board)
|
|
459
463
|
|
|
@@ -162,6 +162,7 @@ class API(object):
|
|
|
162
162
|
self._loop = None
|
|
163
163
|
self._stream_parsers = {}
|
|
164
164
|
self._ssl_context = ssl_init(self.config.verify_ssl)
|
|
165
|
+
self.last_timeout_was_silent = False
|
|
165
166
|
|
|
166
167
|
def _get_event_loop(self):
|
|
167
168
|
if self._loop and not self._loop.is_closed():
|
|
@@ -322,6 +323,7 @@ class API(object):
|
|
|
322
323
|
is_secret = secret
|
|
323
324
|
break_detect_after_send = False
|
|
324
325
|
use_too_many_res = False
|
|
326
|
+
received_any_byte = False
|
|
325
327
|
|
|
326
328
|
while True:
|
|
327
329
|
if refresh and msg and not msg.endswith(command.refresh):
|
|
@@ -369,6 +371,7 @@ class API(object):
|
|
|
369
371
|
if use_too_many_res:
|
|
370
372
|
raise exceptions.UseTooManyResources()
|
|
371
373
|
raise exceptions.ConnectionClosed()
|
|
374
|
+
received_any_byte = True
|
|
372
375
|
|
|
373
376
|
if isinstance(data_chunk, str):
|
|
374
377
|
data_chunk = data_chunk.encode('utf-8')
|
|
@@ -408,6 +411,20 @@ class API(object):
|
|
|
408
411
|
self._RDQ.add(screen)
|
|
409
412
|
if use_too_many_res:
|
|
410
413
|
raise exceptions.UseTooManyResources()
|
|
414
|
+
# ponytail: zero bytes received this send() is NOT proof the
|
|
415
|
+
# connection is dead — pttbbs silently drops invalid keys
|
|
416
|
+
# without redrawing (measured: 5s of zero bytes on the lottery
|
|
417
|
+
# menu after sending a bad key, then the very next `q` exits
|
|
418
|
+
# cleanly in 0.00s, proving the connection was alive the whole
|
|
419
|
+
# time). So this is a hint for the caller, not a death
|
|
420
|
+
# verdict: record whether this round was totally silent and
|
|
421
|
+
# let the caller (e.g. _api_get_user.py) decide what a
|
|
422
|
+
# timeout with no bytes at all should mean for it. Counted
|
|
423
|
+
# per-send, not per-round, on purpose: a multi-round send()
|
|
424
|
+
# that goes silent midway still returns -1, which keeps
|
|
425
|
+
# _api_del_post.py's `timed_out = result == -1` reporting
|
|
426
|
+
# intact ("delete succeeded, but ...") on an irreversible op.
|
|
427
|
+
self.last_timeout_was_silent = not received_any_byte
|
|
411
428
|
return -1
|
|
412
429
|
|
|
413
430
|
if target_index != -1:
|
|
@@ -428,6 +445,8 @@ class API(object):
|
|
|
428
445
|
if self._UseTooManyResources not in target_list:
|
|
429
446
|
target_list = target_list + [self._UseTooManyResources]
|
|
430
447
|
|
|
448
|
+
self.last_timeout_was_silent = False
|
|
449
|
+
|
|
431
450
|
if self.config.connect_mode == data_type.ConnectMode.TELNET:
|
|
432
451
|
# Original Telnet logic remains, as it doesn't use asyncio
|
|
433
452
|
if screen_timeout == 0:
|
|
@@ -463,6 +482,14 @@ class API(object):
|
|
|
463
482
|
try:
|
|
464
483
|
data = self._core.read_very_eager()
|
|
465
484
|
except EOFError:
|
|
485
|
+
# ponytail: known gap — write EOF above raises
|
|
486
|
+
# ConnectionClosed, read EOF here returns -1, so a dead
|
|
487
|
+
# Telnet link can still surface as NoSuchUser/NoSuchBoard
|
|
488
|
+
# upstream (same bug the websocket path was fixed for).
|
|
489
|
+
# Left alone deliberately: raising here carries the
|
|
490
|
+
# _api_del_post.py:351 `timed_out = result == -1`
|
|
491
|
+
# blast radius and there is no Telnet test harness to
|
|
492
|
+
# verify against. Fix when Telnet gets one.
|
|
466
493
|
return -1
|
|
467
494
|
screen = self._stream_screen(self.current_encoding, data)
|
|
468
495
|
screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
|
|
@@ -508,6 +535,22 @@ class API(object):
|
|
|
508
535
|
loop.run_until_complete(asyncio.wait_for(self._core.close(), timeout=2.0))
|
|
509
536
|
except (asyncio.TimeoutError, RuntimeError):
|
|
510
537
|
pass
|
|
538
|
+
# 若 close 逾時,websocket 的背景 task(keepalive_ping/transfer_data/
|
|
539
|
+
# close_connection)會殘留 pending;主動取消並跑完,再關閉 loop,否則 loop 被 GC 時
|
|
540
|
+
# 會噴 "Task was destroyed but it is pending" 與 "no running event loop"。
|
|
541
|
+
if self._loop is not None and not self._loop.is_closed():
|
|
542
|
+
try:
|
|
543
|
+
pending = asyncio.all_tasks(self._loop)
|
|
544
|
+
for task in pending:
|
|
545
|
+
task.cancel()
|
|
546
|
+
if pending:
|
|
547
|
+
self._loop.run_until_complete(
|
|
548
|
+
asyncio.gather(*pending, return_exceptions=True))
|
|
549
|
+
self._loop.run_until_complete(self._loop.shutdown_asyncgens())
|
|
550
|
+
except RuntimeError:
|
|
551
|
+
pass
|
|
552
|
+
finally:
|
|
553
|
+
self._loop.close()
|
|
511
554
|
else:
|
|
512
555
|
self._core.close()
|
|
513
556
|
|
|
@@ -219,27 +219,6 @@ class BoardField(AutoStrEnum):
|
|
|
219
219
|
post_kind_list = auto()
|
|
220
220
|
|
|
221
221
|
|
|
222
|
-
class LotteryField(AutoStrEnum):
|
|
223
|
-
board = auto()
|
|
224
|
-
price = auto()
|
|
225
|
-
total = auto()
|
|
226
|
-
options = auto()
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
class LotteryOptionField(AutoStrEnum):
|
|
230
|
-
index = auto()
|
|
231
|
-
name = auto()
|
|
232
|
-
sold = auto()
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
class LotteryBetField(AutoStrEnum):
|
|
236
|
-
board = auto()
|
|
237
|
-
item = auto()
|
|
238
|
-
name = auto()
|
|
239
|
-
amount = auto()
|
|
240
|
-
cost = auto()
|
|
241
|
-
|
|
242
|
-
|
|
243
222
|
class Compare(AutoStrEnum):
|
|
244
223
|
BIGGER = auto()
|
|
245
224
|
SAME = auto()
|
|
@@ -215,14 +215,6 @@ class NoSuchPost(Error):
|
|
|
215
215
|
return self.message
|
|
216
216
|
|
|
217
217
|
|
|
218
|
-
class NoSuchLottery(Error):
|
|
219
|
-
def __init__(self, board):
|
|
220
|
-
self.message = i18n.no_such_lottery + ': ' + board
|
|
221
|
-
|
|
222
|
-
def __str__(self):
|
|
223
|
-
return self.message
|
|
224
|
-
|
|
225
|
-
|
|
226
218
|
class CanNotUseSearchPostCode(Error):
|
|
227
219
|
"""
|
|
228
220
|
此狀態下無法使用搜尋文章代碼(AID)功能
|
|
@@ -5,7 +5,6 @@ string_data = {
|
|
|
5
5
|
"author": "Author",
|
|
6
6
|
"bad_post": "bad post",
|
|
7
7
|
"bad_post_not_recorded": "the post was deleted, but the bad-post record was not made",
|
|
8
|
-
"bet_lottery": "bet lottery _target0_",
|
|
9
8
|
"board": "Board",
|
|
10
9
|
"can_not_use_search_post_code_f": "This status can not use the search PostField code function",
|
|
11
10
|
"catch_bottom_post": "Catch bottom post",
|
|
@@ -32,7 +31,6 @@ string_data = {
|
|
|
32
31
|
"find_newest_index": "Find newest index",
|
|
33
32
|
"get_board_info": "Get board info _target0_",
|
|
34
33
|
"get_favourite_board_list": "Query favourite board list",
|
|
35
|
-
"get_lottery": "Get lottery info _target0_",
|
|
36
34
|
"get_mail": "Get mail",
|
|
37
35
|
"get_waterball": "Get waterball records",
|
|
38
36
|
"give_money_to": "give _target0_ _target1_ P coins",
|
|
@@ -78,7 +76,6 @@ string_data = {
|
|
|
78
76
|
"no_response": "This post has been closed and marked, no response",
|
|
79
77
|
"no_search_result": "No Search Result",
|
|
80
78
|
"no_such_board": "No such board",
|
|
81
|
-
"no_such_lottery": "No lottery is currently running, or betting is closed",
|
|
82
79
|
"no_such_mail": "No such mail",
|
|
83
80
|
"no_such_post": "In _target0_, the post code is not EXISTS _target1_",
|
|
84
81
|
"no_such_user": "No such user",
|
|
@@ -5,7 +5,6 @@ string_data = {
|
|
|
5
5
|
"author": "作者",
|
|
6
6
|
"bad_post": "劣文",
|
|
7
7
|
"bad_post_not_recorded": "文章已刪除,但退文(惡退)未成功記錄",
|
|
8
|
-
"bet_lottery": "下注樂透 _target0_",
|
|
9
8
|
"board": "看板",
|
|
10
9
|
"can_not_use_search_post_code_f": "此狀態下無法使用搜尋文章代碼(AID)功能",
|
|
11
10
|
"catch_bottom_post": "取得置底文章",
|
|
@@ -32,7 +31,6 @@ string_data = {
|
|
|
32
31
|
"find_newest_index": "找到最新編號",
|
|
33
32
|
"get_board_info": "取得看板資訊 _target0_",
|
|
34
33
|
"get_favourite_board_list": "取得我的最愛",
|
|
35
|
-
"get_lottery": "取得樂透資訊 _target0_",
|
|
36
34
|
"get_mail": "取得信件",
|
|
37
35
|
"get_waterball": "取得水球紀錄",
|
|
38
36
|
"give_money_to": "給 _target0_ _target1_ P 幣",
|
|
@@ -78,7 +76,6 @@ string_data = {
|
|
|
78
76
|
"no_response": "很抱歉, 此文章已結案並標記, 不得回應",
|
|
79
77
|
"no_search_result": "沒有搜尋結果",
|
|
80
78
|
"no_such_board": "無該看板",
|
|
81
|
-
"no_such_lottery": "目前並沒有舉辦樂透或已封盤",
|
|
82
79
|
"no_such_mail": "無此信件",
|
|
83
80
|
"no_such_post": "_target0_ 板找不到這個文章代碼 _target1_",
|
|
84
81
|
"no_such_user": "無該使用者",
|
|
@@ -23,7 +23,6 @@ PyPtt/_api_get_waterball.py
|
|
|
23
23
|
PyPtt/_api_give_money.py
|
|
24
24
|
PyPtt/_api_has_new_mail.py
|
|
25
25
|
PyPtt/_api_loginout.py
|
|
26
|
-
PyPtt/_api_lottery.py
|
|
27
26
|
PyPtt/_api_mail.py
|
|
28
27
|
PyPtt/_api_mark_post.py
|
|
29
28
|
PyPtt/_api_post.py
|
|
@@ -56,9 +55,11 @@ PyPtt/_uao/LICENSE
|
|
|
56
55
|
PyPtt/_uao/__init__.py
|
|
57
56
|
PyPtt/_uao/b2u.py
|
|
58
57
|
PyPtt/_uao/u2b.py
|
|
58
|
+
tests/test_backup_param.py
|
|
59
59
|
tests/test_bucket.py
|
|
60
60
|
tests/test_change_pw.py
|
|
61
61
|
tests/test_comment.py
|
|
62
|
+
tests/test_connect_core_timeout.py
|
|
62
63
|
tests/test_del_post.py
|
|
63
64
|
tests/test_del_post_unit.py
|
|
64
65
|
tests/test_exceptions.py
|
|
@@ -78,12 +79,12 @@ tests/test_get_time.py
|
|
|
78
79
|
tests/test_get_user.py
|
|
79
80
|
tests/test_get_waterball.py
|
|
80
81
|
tests/test_give_money.py
|
|
82
|
+
tests/test_half_dead_connection.py
|
|
81
83
|
tests/test_i18n.py
|
|
82
84
|
tests/test_incremental_parity.py
|
|
83
85
|
tests/test_init.py
|
|
84
86
|
tests/test_logger.py
|
|
85
87
|
tests/test_login_logout.py
|
|
86
|
-
tests/test_lottery.py
|
|
87
88
|
tests/test_mail_send_and_del.py
|
|
88
89
|
tests/test_mark_post.py
|
|
89
90
|
tests/test_parse_query_post.py
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
No-network unit tests for the `backup` parameter of mail / reply_post.
|
|
3
|
+
|
|
4
|
+
Regression guard: `backup` used to skip check_type, so a truthy non-bool
|
|
5
|
+
like the string 'False' silently answered 是否自存底稿 with 'y' and left a
|
|
6
|
+
backup in the sender's mailbox. `reply_post` had no `backup` parameter at
|
|
7
|
+
all and always answered 'Y'.
|
|
8
|
+
"""
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
import PyPtt
|
|
14
|
+
from PyPtt import _api_mail, _api_reply_post
|
|
15
|
+
|
|
16
|
+
NON_BOOLS = ['False', 'false', '0', 0, 1, None]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _StubAPI:
|
|
20
|
+
"""Just enough of PyPtt.API to reach the parameter checks."""
|
|
21
|
+
_thread_id = threading.get_ident()
|
|
22
|
+
_is_login = True
|
|
23
|
+
is_registered_user = True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize('backup', NON_BOOLS)
|
|
27
|
+
def test_mail_non_bool_backup_raises(backup):
|
|
28
|
+
with pytest.raises(TypeError):
|
|
29
|
+
_api_mail.mail(_StubAPI(), 'someone', 'title', 'content', 0, backup)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize('backup', NON_BOOLS)
|
|
33
|
+
def test_reply_post_non_bool_backup_raises(backup):
|
|
34
|
+
with pytest.raises(TypeError):
|
|
35
|
+
_api_reply_post.reply_post(_StubAPI(), PyPtt.ReplyTo.BOARD, 'Test', 'content', 0, None, 1, backup)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_reply_post_exposes_backup():
|
|
39
|
+
"""回信給作者時的自存底稿以前寫死 'Y',關不掉。"""
|
|
40
|
+
import inspect
|
|
41
|
+
assert inspect.signature(PyPtt.API.reply_post).parameters['backup'].default is True
|