PyPtt 2.1.5__tar.gz → 2.1.7__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.1.5 → pyptt-2.1.7}/PKG-INFO +1 -1
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/__init__.py +1 -1
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_del_post.py +107 -2
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_newest_index.py +1 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_post.py +1 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_mail.py +15 -0
- pyptt-2.1.7/PyPtt/ssl_config.py +24 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt.egg-info/PKG-INFO +1 -1
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt.egg-info/SOURCES.txt +1 -0
- pyptt-2.1.7/tests/test_del_post_unit.py +78 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_mail.py +48 -0
- pyptt-2.1.5/PyPtt/ssl_config.py +0 -24
- {pyptt-2.1.5 → pyptt-2.1.7}/LICENSE +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/PTT.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_bucket.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_call_status.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_change_pw.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_comment.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_board_info.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_board_list.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_bottom_post_list.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_favourite_board.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_post_index.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_post_list.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_time.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_user.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_get_waterball.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_give_money.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_has_new_mail.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_loginout.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_mark_post.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_post.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_reply_post.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_search_user.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_set_board_title.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/_api_util.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/api_server.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/check_value.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/command.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/config.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/connect_core.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/data_type.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/exceptions.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/i18n.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/lang_en_US.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/lang_zh_TW.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/lib_util.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/log.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/screens.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt/service.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt.egg-info/dependency_links.txt +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt.egg-info/requires.txt +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/PyPtt.egg-info/top_level.txt +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/README.md +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/pyproject.toml +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/setup.cfg +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/setup.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_bucket.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_change_pw.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_comment.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_del_post.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_exceptions.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_fast_post.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_board_info.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_board_list.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_bottom_post_list.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_content_formula.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_favourite_boards.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_newest_index.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_post.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_post_list.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_post_list_parser.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_post_parser.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_time.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_user.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_get_waterball.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_give_money.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_i18n.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_init.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_logger.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_login_logout.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_mail_send_and_del.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_mark_post.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_parse_query_post.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_post.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_pyte_parity.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_reply_post.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_search_user.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_service.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_set_board_title.py +0 -0
- {pyptt-2.1.5 → pyptt-2.1.7}/tests/test_vt100_parser.py +0 -0
|
@@ -17,6 +17,45 @@ from . import screens
|
|
|
17
17
|
# character), not in Python characters.
|
|
18
18
|
_BAD_POST_REASON_MAX_BYTES = 50
|
|
19
19
|
|
|
20
|
+
# --- del_post's `reason` field (annotates the title shown after a
|
|
21
|
+
# moderator deletes another user's post) ---
|
|
22
|
+
#
|
|
23
|
+
# pttbbs pre-fills this prompt with "(已被<moderator id>刪除) <author id>"
|
|
24
|
+
# (pttbbs mbbsd/bbs.c, del_post()'s SAFE_ARTICLE_DELETE branch) in a
|
|
25
|
+
# `char reason[PROPER_TITLE_LEN]` buffer; PROPER_TITLE_LEN is 42
|
|
26
|
+
# (pttbbs include/common.h), so the whole title -- prefix *and* PyPtt's
|
|
27
|
+
# reason together -- is capped at 41 Big5 bytes, not python characters.
|
|
28
|
+
# PyPtt answers the prompt with Ctrl-E (jump to the end of the prefilled
|
|
29
|
+
# text) plus a leading space plus `reason`, so the byte budget actually
|
|
30
|
+
# left for `reason` depends on how long the moderator's and the post
|
|
31
|
+
# author's PTT IDs are -- there is no single safe constant independent of
|
|
32
|
+
# who is deleting what, unlike bad_post_reason above.
|
|
33
|
+
#
|
|
34
|
+
# Verified live against a local pttbbs (bbsdocker/imageptt rebuilt with
|
|
35
|
+
# SAFE_ARTICLE_DELETE enabled -- the stock image ships with that feature
|
|
36
|
+
# #define'd out and never offers this prompt at all):
|
|
37
|
+
# - exactly at the budget: reason is accepted in full, title not
|
|
38
|
+
# truncated.
|
|
39
|
+
# - 1 byte over budget, ASCII-only overflow: pttbbs silently truncates
|
|
40
|
+
# to fit (title is just missing the last character).
|
|
41
|
+
# - 1 byte over budget, where the overflow splits a double-byte Big5
|
|
42
|
+
# glyph in half: pttbbs's line editor corrupts and del_post() hangs
|
|
43
|
+
# until connect_core's screen_timeout, returning neither success nor
|
|
44
|
+
# a clean failure -- this is the "刪文失敗" a caller with a too-long
|
|
45
|
+
# reason actually experiences.
|
|
46
|
+
# PyPtt must reject before sending in both cases.
|
|
47
|
+
_REASON_TITLE_BUFFER_BYTES = 41 # PROPER_TITLE_LEN(42) - 1 byte reserved for the NUL terminator
|
|
48
|
+
_REASON_PREFIX_FIXED_BYTES = 13 # Big5 bytes of "(已被刪除) <>", the literal part of "(已被%s刪除) <%s>"
|
|
49
|
+
# A rough, moderator/author-id-independent ceiling: the most bytes `reason`
|
|
50
|
+
# could ever fit under, i.e. the budget if both PTT ids were the shortest
|
|
51
|
+
# possible (1 byte each). Real ids can only be longer than that, which only
|
|
52
|
+
# shrinks the true budget further -- so a reason within this ceiling is not
|
|
53
|
+
# guaranteed to fit any *particular* id pair, but a reason over it can never
|
|
54
|
+
# fit *any* id pair. That asymmetry is what makes it safe to use as an
|
|
55
|
+
# early, id-independent guard: it will never reject a reason that might be
|
|
56
|
+
# legal once we know the real ids (checked later by _check_reason_budget).
|
|
57
|
+
_REASON_MAX_POSSIBLE_BYTES = _REASON_TITLE_BUFFER_BYTES - _REASON_PREFIX_FIXED_BYTES - 1 - 1 - 1
|
|
58
|
+
|
|
20
59
|
|
|
21
60
|
def _check_no_control_chars(name: str, value: str) -> None:
|
|
22
61
|
"""Terminal command injection guard: none of PyPtt's del_post text
|
|
@@ -40,6 +79,55 @@ def _check_bad_post_reason(bad_post_reason: str) -> None:
|
|
|
40
79
|
f'bad_post_reason must not exceed {_BAD_POST_REASON_MAX_BYTES} bytes')
|
|
41
80
|
|
|
42
81
|
|
|
82
|
+
def _check_reason(reason: str) -> None:
|
|
83
|
+
"""Early, post_info-independent guard for `reason`: control chars,
|
|
84
|
+
Big5-encodability, and a rough length ceiling that can never fit any
|
|
85
|
+
moderator/author id pair (_REASON_MAX_POSSIBLE_BYTES). This alone does
|
|
86
|
+
NOT guarantee `reason` fits the *actual* ids involved -- that exact,
|
|
87
|
+
id-dependent budget is enforced later by _check_reason_budget, once
|
|
88
|
+
post_info is available. Keeping this one id-independent lets del_post()
|
|
89
|
+
reject an obviously-broken reason (control chars, non-Big5, absurdly
|
|
90
|
+
long) the same way regardless of whether the target post still exists."""
|
|
91
|
+
_check_no_control_chars('reason', reason)
|
|
92
|
+
try:
|
|
93
|
+
encoded = reason.encode('big5uao')
|
|
94
|
+
except UnicodeEncodeError as e:
|
|
95
|
+
raise exceptions.ParameterError(
|
|
96
|
+
f'reason contains a character that cannot be encoded in Big5: {e}') from e
|
|
97
|
+
if len(encoded) > _REASON_MAX_POSSIBLE_BYTES:
|
|
98
|
+
raise exceptions.ParameterError(
|
|
99
|
+
f'reason must not exceed {_REASON_MAX_POSSIBLE_BYTES} bytes '
|
|
100
|
+
f'(pttbbs caps the deleted-post title at {_REASON_TITLE_BUFFER_BYTES} bytes total; '
|
|
101
|
+
f'{_REASON_MAX_POSSIBLE_BYTES} bytes is the most any reason could ever fit, for the '
|
|
102
|
+
f'shortest possible moderator/author PTT ids -- the actual limit for this delete may '
|
|
103
|
+
f'be smaller)')
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _check_reason_budget(reason: str, moderator_id: str, author_id: str) -> None:
|
|
107
|
+
"""Exact, post_info-dependent guard for `reason`: pttbbs pre-fills the
|
|
108
|
+
delete-title prompt with "(已被<moderator_id>刪除) <author_id>" inside the
|
|
109
|
+
fixed-size buffer described above, so the byte budget actually left for
|
|
110
|
+
`reason` depends on the two real PTT IDs involved in this delete --
|
|
111
|
+
unlike _check_bad_post_reason, this can't be a plain module-level
|
|
112
|
+
constant. Call this once post_info is available (see del_post());
|
|
113
|
+
_check_reason above already covers the id-independent checks (control
|
|
114
|
+
chars, Big5-encodability) so this only needs to re-derive the byte
|
|
115
|
+
count for the length comparison."""
|
|
116
|
+
try:
|
|
117
|
+
encoded = reason.encode('big5uao')
|
|
118
|
+
except UnicodeEncodeError as e:
|
|
119
|
+
raise exceptions.ParameterError(
|
|
120
|
+
f'reason contains a character that cannot be encoded in Big5: {e}') from e
|
|
121
|
+
prefix_bytes = _REASON_PREFIX_FIXED_BYTES + len(moderator_id) + len(author_id)
|
|
122
|
+
# -1 for the leading space PyPtt sends before reason (see del_post()).
|
|
123
|
+
max_reason_bytes = max(0, _REASON_TITLE_BUFFER_BYTES - prefix_bytes - 1)
|
|
124
|
+
if len(encoded) > max_reason_bytes:
|
|
125
|
+
raise exceptions.ParameterError(
|
|
126
|
+
f'reason must not exceed {max_reason_bytes} bytes for this moderator/author pair '
|
|
127
|
+
f'(pttbbs caps the deleted-post title at {_REASON_TITLE_BUFFER_BYTES} bytes total, '
|
|
128
|
+
f'and the prefix "(已被{moderator_id}刪除) <{author_id}>" already uses {prefix_bytes} of them)')
|
|
129
|
+
|
|
130
|
+
|
|
43
131
|
def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0, reason: Optional[str] = None,
|
|
44
132
|
bad_post_type: Optional[data_type.BadPostType] = None,
|
|
45
133
|
bad_post_reason: Optional[str] = None) -> None:
|
|
@@ -74,9 +162,17 @@ def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int =
|
|
|
74
162
|
# --- Pure parameter validation (doesn't need post_info) happens here,
|
|
75
163
|
# before the "already deleted" early-return below -- an invalid
|
|
76
164
|
# combination of parameters must always fail the same way, regardless of
|
|
77
|
-
# whether the post happens to already be gone.
|
|
165
|
+
# whether the post happens to already be gone. `reason` is checked in
|
|
166
|
+
# two stages for this reason: _check_reason here covers everything that
|
|
167
|
+
# doesn't depend on state (control chars, Big5-encodability, and a
|
|
168
|
+
# rough ceiling no id pair could ever exceed), so those failures are
|
|
169
|
+
# always reported the same way; the *exact* budget depends on the
|
|
170
|
+
# moderator's and the post author's real PTT IDs, so that part
|
|
171
|
+
# (_check_reason_budget) has to wait until post_info is available below
|
|
172
|
+
# -- alongside the other post_info-dependent reason check ("reason is
|
|
173
|
+
# only valid when...") a few lines down. ---
|
|
78
174
|
if reason is not None:
|
|
79
|
-
|
|
175
|
+
_check_reason(reason)
|
|
80
176
|
|
|
81
177
|
if bad_post_type == data_type.BadPostType.OTHER:
|
|
82
178
|
if not bad_post_reason:
|
|
@@ -132,6 +228,15 @@ def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int =
|
|
|
132
228
|
log.logger.info(i18n.delete_post, '...', i18n.fail)
|
|
133
229
|
raise exceptions.NoPermission(i18n.no_permission)
|
|
134
230
|
|
|
231
|
+
# Reaching here with reason still set means: not self-authored (checked
|
|
232
|
+
# above) and permitted (the check_author gate above didn't raise) --
|
|
233
|
+
# i.e. exactly the "moderator deleting someone else's post" case where
|
|
234
|
+
# PTT will actually offer the R加註理由 prompt. Validate reason's exact
|
|
235
|
+
# byte budget now, using the real moderator/author id pair, before any
|
|
236
|
+
# of the interactive delete flow below is sent.
|
|
237
|
+
if reason:
|
|
238
|
+
_check_reason_budget(reason, api.ptt_id, post_info[data_type.PostField.author])
|
|
239
|
+
|
|
135
240
|
_api_util.goto_board(api, board)
|
|
136
241
|
|
|
137
242
|
cmd_list = []
|
|
@@ -65,6 +65,7 @@ def get_newest_index(api, index_type: data_type.NewIndex, board: Optional[str] =
|
|
|
65
65
|
search_list = []
|
|
66
66
|
else:
|
|
67
67
|
check_value.check_type(search_list, list, 'search_list')
|
|
68
|
+
search_list = list(search_list) # 別就地改到呼叫端傳進來的 list
|
|
68
69
|
|
|
69
70
|
if (search_type, search_condition) != (None, None):
|
|
70
71
|
search_list.insert(0, (search_type, search_condition))
|
|
@@ -39,6 +39,7 @@ def get_post(api, board: str, aid: Optional[str] = None, index: Optional[int] =
|
|
|
39
39
|
search_list = []
|
|
40
40
|
else:
|
|
41
41
|
check_value.check_type(search_list, list, 'search_list')
|
|
42
|
+
search_list = list(search_list) # 別就地改到呼叫端傳進來的 list
|
|
42
43
|
|
|
43
44
|
if (search_type, search_condition) != (None, None):
|
|
44
45
|
search_list.insert(0, (search_type, search_condition))
|
|
@@ -146,6 +146,21 @@ def get_mail(api, index: int, search_type: Optional[data_type.SearchType] = None
|
|
|
146
146
|
cmd_list.append(command.ctrl_z)
|
|
147
147
|
cmd_list.append('m')
|
|
148
148
|
|
|
149
|
+
# search_type/search_condition 併入 search_list(比照 get_post),
|
|
150
|
+
# 否則單獨使用 search_type 搜尋會被忽略。
|
|
151
|
+
if search_list is None:
|
|
152
|
+
search_list = []
|
|
153
|
+
else:
|
|
154
|
+
check_value.check_type(search_list, list, 'search_list')
|
|
155
|
+
search_list = list(search_list) # 別就地改到呼叫端傳進來的 list
|
|
156
|
+
|
|
157
|
+
if (search_type, search_condition) != (None, None):
|
|
158
|
+
search_list.insert(0, (search_type, search_condition))
|
|
159
|
+
|
|
160
|
+
for search_type, search_condition in search_list:
|
|
161
|
+
check_value.check_type(search_type, data_type.SearchType, 'search_type')
|
|
162
|
+
check_value.check_type(search_condition, str, 'search_condition')
|
|
163
|
+
|
|
149
164
|
# 處理條件整理出指令
|
|
150
165
|
_cmd_list = _api_util.get_search_condition_cmd(data_type.NewIndex.MAIL, search_list)
|
|
151
166
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
key = """-----BEGIN EC PARAMETERS-----
|
|
2
|
+
BggqhkjOPQMBBw==
|
|
3
|
+
-----END EC PARAMETERS-----
|
|
4
|
+
-----BEGIN EC PRIVATE KEY-----
|
|
5
|
+
MHcCAQEEIBl6n84gGhyoI6rh8jsDM6+MvbtX++1SctY8mkfAU/PqoAoGCCqGSM49
|
|
6
|
+
AwEHoUQDQgAEKrKMWokWd4Y6dFHFFr4pg9h340NKkekdCrYbxv1l2SUBUDkXUx28
|
|
7
|
+
fGqtXxYCuOsEQTeQrm0OmCq6U+aRXMvwaQ==
|
|
8
|
+
-----END EC PRIVATE KEY-----
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
cert = """-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIB6DCCAY8CFEXMlgDToWo6s00YgTOitW0BgNCdMAoGCCqGSM49BAMCMHcxCzAJ
|
|
13
|
+
BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
|
|
14
|
+
CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
|
|
15
|
+
EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA3MTgwNDE0NTZaFw0zNjA3MTUwNDE0
|
|
16
|
+
NTZaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
|
|
17
|
+
eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
|
|
18
|
+
bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
|
|
19
|
+
AwEHA0IABCqyjFqJFneGOnRRxRa+KYPYd+NDSpHpHQq2G8b9ZdklAVA5F1MdvHxq
|
|
20
|
+
rV8WArjrBEE3kK5tDpgqulPmkVzL8GkwCgYIKoZIzj0EAwIDRwAwRAIgcfQdHGfL
|
|
21
|
+
m1j+zvm23ToVfbfGdlvcR4hXBQkWMQslAkICIEccC2jqieHYgaLRUgzoUVI/twm3
|
|
22
|
+
5uAjX0ZWPxTPuNvA
|
|
23
|
+
-----END CERTIFICATE-----
|
|
24
|
+
"""
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
No-network unit tests for the pure validation helpers in _api_del_post.py.
|
|
3
|
+
These call the module functions directly and don't need a live PTT session.
|
|
4
|
+
"""
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import PyPtt
|
|
8
|
+
from PyPtt import _api_del_post
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestCheckBadPostReason:
|
|
12
|
+
def test_within_limit_ok(self):
|
|
13
|
+
_api_del_post._check_bad_post_reason('測' * 25) # 50 bytes, exactly at the limit
|
|
14
|
+
|
|
15
|
+
def test_over_limit_raises(self):
|
|
16
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
17
|
+
_api_del_post._check_bad_post_reason('測' * 25 + 'x') # 51 bytes
|
|
18
|
+
|
|
19
|
+
def test_control_char_raises(self):
|
|
20
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
21
|
+
_api_del_post._check_bad_post_reason('bad\x07reason')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestCheckReasonStatic:
|
|
25
|
+
"""`_check_reason` is the early, post_info-independent guard: control
|
|
26
|
+
chars, Big5-encodability, and a rough ceiling (_REASON_MAX_POSSIBLE_BYTES
|
|
27
|
+
== 41 - 13 - 1 - 1 - 1 == 25 bytes) that assumes the shortest possible
|
|
28
|
+
(1-byte) moderator/author PTT ids -- the most bytes a reason could ever
|
|
29
|
+
fit under any real id pair. It therefore never rejects a reason that
|
|
30
|
+
might be legal once the real ids are known; that exact, smaller,
|
|
31
|
+
id-dependent budget is _check_reason_budget's job (see below)."""
|
|
32
|
+
|
|
33
|
+
def test_within_rough_ceiling_ok(self):
|
|
34
|
+
_api_del_post._check_reason('測' * 12 + 'x') # 25 bytes, exactly the rough ceiling
|
|
35
|
+
|
|
36
|
+
def test_over_rough_ceiling_raises(self):
|
|
37
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
38
|
+
_api_del_post._check_reason('測' * 12 + 'xx') # 26 bytes
|
|
39
|
+
|
|
40
|
+
def test_control_char_raises(self):
|
|
41
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
42
|
+
_api_del_post._check_reason('bad\x07reason')
|
|
43
|
+
|
|
44
|
+
def test_non_big5_raises(self):
|
|
45
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
46
|
+
_api_del_post._check_reason('emoji not in big5uao \U0001F600')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestCheckReasonBudget:
|
|
50
|
+
"""`_check_reason_budget` is the exact, post_info-dependent guard: reason
|
|
51
|
+
gets appended, after a leading space, to pttbbs's prefilled
|
|
52
|
+
"(已被<moderator_id>刪除) <author_id>" title inside a 41-Big5-byte buffer
|
|
53
|
+
(_REASON_TITLE_BUFFER_BYTES). Budget for a 9-char/9-char id pair (e.g.
|
|
54
|
+
pypttbot1 deleting pypttbot2's post, as used in the local imageptt
|
|
55
|
+
integration tests) is 41 - 13 - 9 - 9 - 1 == 9 bytes -- confirmed live
|
|
56
|
+
against a local pttbbs rebuilt with SAFE_ARTICLE_DELETE enabled."""
|
|
57
|
+
|
|
58
|
+
MOD_ID = 'pypttbot1'
|
|
59
|
+
AUTHOR_ID = 'pypttbot2'
|
|
60
|
+
|
|
61
|
+
def test_within_limit_ok(self):
|
|
62
|
+
_api_del_post._check_reason_budget('測測測測x', self.MOD_ID, self.AUTHOR_ID) # 9 bytes, exactly at the limit
|
|
63
|
+
|
|
64
|
+
def test_over_limit_by_one_byte_raises(self):
|
|
65
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
66
|
+
_api_del_post._check_reason_budget('測測測測測', self.MOD_ID, self.AUTHOR_ID) # 10 bytes
|
|
67
|
+
|
|
68
|
+
def test_over_limit_ascii_raises(self):
|
|
69
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
70
|
+
_api_del_post._check_reason_budget('x' * 10, self.MOD_ID, self.AUTHOR_ID)
|
|
71
|
+
|
|
72
|
+
def test_budget_shrinks_with_longer_ids(self):
|
|
73
|
+
"""A 12-char/12-char id pair (PTT's real max, IDLEN==12) leaves only
|
|
74
|
+
3 bytes of budget -- a reason that fits the 9/9 pair above no
|
|
75
|
+
longer fits."""
|
|
76
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
77
|
+
_api_del_post._check_reason_budget('測測測測x', 'a' * 12, 'b' * 12)
|
|
78
|
+
_api_del_post._check_reason_budget('x', 'a' * 12, 'b' * 12) # 1 byte still fits the 3-byte budget
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import PyPtt
|
|
2
2
|
import pytest
|
|
3
3
|
|
|
4
|
+
from PyPtt import _api_mail, _api_util, data_type
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
def _handle_empty_mailbox(ptt_bot):
|
|
6
8
|
"""On LOCALHOST, scripts/bootstrap_local_pttbbs.py always sends each bot a
|
|
@@ -14,6 +16,52 @@ def _handle_empty_mailbox(ptt_bot):
|
|
|
14
16
|
"so this indicates a real regression, not an environment quirk.")
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
def test_get_mail_search_type_is_honored(monkeypatch):
|
|
20
|
+
"""Regression: get_mail must merge search_type/search_condition into the
|
|
21
|
+
mail search command, the same way get_newest_index does. They were
|
|
22
|
+
previously ignored, so `get_mail(index, search_type=AUTHOR, ...)` returned
|
|
23
|
+
the unfiltered mail at that index instead of the author-filtered one.
|
|
24
|
+
|
|
25
|
+
Offline unit test: captures the search_list handed to the command builder
|
|
26
|
+
and stops before any PTT navigation. (log/i18n are initialised by the
|
|
27
|
+
autouse ptt_bots fixture, which constructs PyPtt.API before this runs.)"""
|
|
28
|
+
|
|
29
|
+
class _StopBeforeNetwork(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
captured = {}
|
|
33
|
+
|
|
34
|
+
def fake_cmd(index_type, search_list):
|
|
35
|
+
captured["search_list"] = search_list
|
|
36
|
+
raise _StopBeforeNetwork
|
|
37
|
+
|
|
38
|
+
monkeypatch.setattr(_api_util, "one_thread", lambda api: None)
|
|
39
|
+
monkeypatch.setattr(_api_util, "get_search_condition_cmd", fake_cmd)
|
|
40
|
+
|
|
41
|
+
class _FakeAPI:
|
|
42
|
+
_is_login = True
|
|
43
|
+
is_registered_user = True
|
|
44
|
+
|
|
45
|
+
def get_newest_index(self, *a, **k):
|
|
46
|
+
return 999
|
|
47
|
+
|
|
48
|
+
# search_type/search_condition alone (no search_list) must reach the builder.
|
|
49
|
+
# Both the enum and its string name are accepted (SearchType._MagicMeta lets
|
|
50
|
+
# isinstance('AUTHOR', SearchType) pass), matching get_post's validation loop.
|
|
51
|
+
for st in ("AUTHOR", data_type.SearchType.AUTHOR):
|
|
52
|
+
captured.clear()
|
|
53
|
+
with pytest.raises(_StopBeforeNetwork):
|
|
54
|
+
_api_mail.get_mail(_FakeAPI(), index=3, search_type=st,
|
|
55
|
+
search_condition="someone")
|
|
56
|
+
assert captured["search_list"] == [(st, "someone")]
|
|
57
|
+
|
|
58
|
+
# no search -> no filter (unchanged behaviour)
|
|
59
|
+
captured.clear()
|
|
60
|
+
with pytest.raises(_StopBeforeNetwork):
|
|
61
|
+
_api_mail.get_mail(_FakeAPI(), index=1)
|
|
62
|
+
assert captured["search_list"] == []
|
|
63
|
+
|
|
64
|
+
|
|
17
65
|
def test_get_newest_mail(ptt_bots):
|
|
18
66
|
"""Tests getting the newest mail from the mailbox."""
|
|
19
67
|
verified = False
|
pyptt-2.1.5/PyPtt/ssl_config.py
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
key = """-----BEGIN EC PARAMETERS-----
|
|
2
|
-
BggqhkjOPQMBBw==
|
|
3
|
-
-----END EC PARAMETERS-----
|
|
4
|
-
-----BEGIN EC PRIVATE KEY-----
|
|
5
|
-
MHcCAQEEIF0fyzVzlWVfbwf0IkWdOi1DD6g4dl2Wf2MSaPKS2pRtoAoGCCqGSM49
|
|
6
|
-
AwEHoUQDQgAEvsTkmHxvh9EMh+1wFd+Z8bN0bLOWisGv3uNP0qeV0jy88cmPTWRl
|
|
7
|
-
OzU8emmKTJZpuoOEGqi/Ow4vo+WTxMPynQ==
|
|
8
|
-
-----END EC PRIVATE KEY-----
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
cert = """-----BEGIN CERTIFICATE-----
|
|
12
|
-
MIIB6TCCAY8CFGi0fucoS9ysCrt9pKAu9BgMAULWMAoGCCqGSM49BAMCMHcxCzAJ
|
|
13
|
-
BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
|
|
14
|
-
CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
|
|
15
|
-
EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA3MTgwMTIwNTlaFw0zNjA3MTUwMTIw
|
|
16
|
-
NTlaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
|
|
17
|
-
eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
|
|
18
|
-
bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
|
|
19
|
-
AwEHA0IABL7E5Jh8b4fRDIftcBXfmfGzdGyzlorBr97jT9KnldI8vPHJj01kZTs1
|
|
20
|
-
PHppikyWabqDhBqovzsOL6Plk8TD8p0wCgYIKoZIzj0EAwIDSAAwRQIgYTfmCpJN
|
|
21
|
-
abcPTvASZb/a7vanhO1vpafJ6Hlff6pAxh0CIQCl/Vvrx6DV21rUC3rkHBOFeqev
|
|
22
|
-
I9Izuv9PxfLpnxoFcw==
|
|
23
|
-
-----END CERTIFICATE-----
|
|
24
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|