PyPtt 2.1.1__tar.gz → 2.1.2__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.1 → pyptt-2.1.2}/PKG-INFO +1 -1
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/PTT.py +5 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/__init__.py +1 -1
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_get_board_list.py +4 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_get_favourite_board.py +1 -0
- pyptt-2.1.2/PyPtt/_api_get_post_list.py +111 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_get_waterball.py +45 -44
- pyptt-2.1.2/PyPtt/_api_has_new_mail.py +77 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_util.py +167 -136
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/config.py +5 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/connect_core.py +23 -2
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/screens.py +2 -2
- pyptt-2.1.2/PyPtt/ssl_config.py +24 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt.egg-info/PKG-INFO +1 -1
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt.egg-info/SOURCES.txt +1 -0
- pyptt-2.1.2/tests/test_get_content_formula.py +431 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_post_list_parser.py +54 -0
- pyptt-2.1.1/PyPtt/_api_get_post_list.py +0 -104
- pyptt-2.1.1/PyPtt/_api_has_new_mail.py +0 -76
- pyptt-2.1.1/PyPtt/ssl_config.py +0 -24
- {pyptt-2.1.1 → pyptt-2.1.2}/LICENSE +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_bucket.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_call_status.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_change_pw.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_comment.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_del_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_get_board_info.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_get_bottom_post_list.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_get_newest_index.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_get_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_get_post_index.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_get_time.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_get_user.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_give_money.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_loginout.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_mail.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_mark_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_reply_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_search_user.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/_api_set_board_title.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/api_server.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/check_value.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/command.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/data_type.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/exceptions.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/i18n.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/lang_en_US.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/lang_zh_TW.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/lib_util.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/log.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt/service.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt.egg-info/dependency_links.txt +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt.egg-info/requires.txt +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/PyPtt.egg-info/top_level.txt +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/README.md +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/pyproject.toml +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/setup.cfg +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/setup.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_bucket.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_change_pw.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_comment.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_del_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_exceptions.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_fast_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_board_info.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_board_list.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_bottom_post_list.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_favourite_boards.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_mail.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_newest_index.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_post_list.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_post_parser.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_time.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_user.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_get_waterball.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_give_money.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_i18n.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_init.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_logger.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_mail_send_and_del.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_mark_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_parse_query_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_pyte_parity.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_reply_post.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_search_user.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_service.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_set_board_title.py +0 -0
- {pyptt-2.1.1 → pyptt-2.1.2}/tests/test_vt100_parser.py +0 -0
|
@@ -150,6 +150,11 @@ class API:
|
|
|
150
150
|
check_value.check_type(verify_ssl, bool, 'verify_ssl')
|
|
151
151
|
self.config.verify_ssl = verify_ssl
|
|
152
152
|
|
|
153
|
+
screen_height = kwargs.get('screen_height', 24)
|
|
154
|
+
check_value.check_type(screen_height, int, 'screen_height')
|
|
155
|
+
check_value.check_range(screen_height, 24, 254, 'screen_height')
|
|
156
|
+
self.config.screen_height = screen_height
|
|
157
|
+
|
|
153
158
|
self.connect_core = connect_core.API(self)
|
|
154
159
|
self._exist_board_list = set()
|
|
155
160
|
self._exist_user_list = set()
|
|
@@ -65,6 +65,10 @@ def get_board_list(api) -> list:
|
|
|
65
65
|
cmd = ''.join(cmd_list)
|
|
66
66
|
|
|
67
67
|
board_list = []
|
|
68
|
+
# NOTE: expanded_screen is intentionally NOT used here.
|
|
69
|
+
# get_board_list traverses all boards sequentially; a mid-session NAWS resize
|
|
70
|
+
# causes PTT to render the first page at the old size before the resize takes
|
|
71
|
+
# effect, producing inconsistent page boundaries and skipping boards.
|
|
68
72
|
while True:
|
|
69
73
|
|
|
70
74
|
api.connect_core.send(
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
|
|
3
|
+
from . import _api_util
|
|
4
|
+
from . import check_value
|
|
5
|
+
from . import command
|
|
6
|
+
from . import connect_core
|
|
7
|
+
from . import data_type
|
|
8
|
+
from . import exceptions
|
|
9
|
+
from . import i18n
|
|
10
|
+
from . import log
|
|
11
|
+
from . import screens
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_post_list(api, board: str, limit: int = 20, offset: int = 0) -> List[Dict]:
|
|
15
|
+
_api_util.one_thread(api)
|
|
16
|
+
|
|
17
|
+
if not api._is_login:
|
|
18
|
+
raise exceptions.RequireLogin(i18n.require_login)
|
|
19
|
+
|
|
20
|
+
check_value.check_type(board, str, 'board')
|
|
21
|
+
if limit is not None:
|
|
22
|
+
check_value.check_type(limit, int, 'limit')
|
|
23
|
+
if offset is not None:
|
|
24
|
+
check_value.check_type(offset, int, 'offset')
|
|
25
|
+
|
|
26
|
+
max_index = api.get_newest_index(data_type.NewIndex.BOARD, board=board)
|
|
27
|
+
|
|
28
|
+
check_value.check_range(limit, min_value=1, max_value=max_index, name='limit')
|
|
29
|
+
check_value.check_range(offset, min_value=0, max_value=max_index - 1, name='offset')
|
|
30
|
+
|
|
31
|
+
start_index = max_index - limit - offset + 1
|
|
32
|
+
end_index = start_index + limit - 1
|
|
33
|
+
|
|
34
|
+
# the first page shows 10 posts, each next page shows 20 posts
|
|
35
|
+
if limit <= 10:
|
|
36
|
+
max_round = 1
|
|
37
|
+
else:
|
|
38
|
+
# 1 is for the first page, +1 is for rounding up
|
|
39
|
+
# so +2
|
|
40
|
+
max_round = (limit - 10) // 20 + 2
|
|
41
|
+
|
|
42
|
+
cmd_list = []
|
|
43
|
+
|
|
44
|
+
cmd_list.append(str(max(1, start_index - 100)))
|
|
45
|
+
cmd_list.append(command.enter)
|
|
46
|
+
cmd_list.append(str(start_index))
|
|
47
|
+
cmd_list.append(command.enter)
|
|
48
|
+
|
|
49
|
+
cmd = ''.join(cmd_list)
|
|
50
|
+
|
|
51
|
+
target_list = [
|
|
52
|
+
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True)
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
post_list = []
|
|
56
|
+
|
|
57
|
+
with _api_util.expanded_screen(api):
|
|
58
|
+
for _ in range(max_round):
|
|
59
|
+
|
|
60
|
+
index = api.connect_core.send(cmd, target_list)
|
|
61
|
+
cmd = command.page_down
|
|
62
|
+
last_screen = api.connect_core.get_screen_queue()[-1]
|
|
63
|
+
|
|
64
|
+
is_parse_area = False
|
|
65
|
+
|
|
66
|
+
for line in last_screen.splitlines()[3:-1]:
|
|
67
|
+
|
|
68
|
+
if not line.startswith(api.cursor) and not is_parse_area:
|
|
69
|
+
continue
|
|
70
|
+
is_parse_area = True
|
|
71
|
+
|
|
72
|
+
if line.strip().startswith(api.cursor):
|
|
73
|
+
# replace the first cursor in the line
|
|
74
|
+
line = line.replace(api.cursor, ' ' * len(api.cursor.encode('big5uao')), 1)
|
|
75
|
+
|
|
76
|
+
if api.cursor == data_type.Cursor.NEW:
|
|
77
|
+
# Pad one leading space so the fixed-column offsets below
|
|
78
|
+
# apply equally to cursor and non-cursor rows. Before the
|
|
79
|
+
# VT100Parser cell-tracking fix, the parser inserted this
|
|
80
|
+
# padding implicitly via its off-by-one column quirk; the
|
|
81
|
+
# offsets here were tuned to that padded layout.
|
|
82
|
+
line = f' {line}'
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
cur_index = int(line[:8].strip())
|
|
86
|
+
except ValueError:
|
|
87
|
+
# Pinned/announcement posts use '★' instead of a numeric
|
|
88
|
+
# index — they have no sequential index and cannot be
|
|
89
|
+
# fetched by index, so skip them.
|
|
90
|
+
continue
|
|
91
|
+
status = line[8:10].strip()
|
|
92
|
+
comment = line[10:12].strip()
|
|
93
|
+
list_date = line[12:17].strip()
|
|
94
|
+
author = line[18:31].strip()
|
|
95
|
+
title = line[31:]
|
|
96
|
+
|
|
97
|
+
cur_post = {
|
|
98
|
+
data_type.PostField.board: board,
|
|
99
|
+
data_type.PostField.index: cur_index,
|
|
100
|
+
data_type.PostField.list_date: list_date,
|
|
101
|
+
data_type.PostField.author: author,
|
|
102
|
+
data_type.PostField.title: title,
|
|
103
|
+
data_type.PostField.push_number: comment
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
post_list.append(cur_post)
|
|
107
|
+
|
|
108
|
+
if cur_index >= end_index:
|
|
109
|
+
return post_list
|
|
110
|
+
|
|
111
|
+
raise exceptions.UnknownError(i18n.unknown_error)
|
|
@@ -101,51 +101,52 @@ def get_waterball(api, post_action: WaterballPostAction = WaterballPostAction.KE
|
|
|
101
101
|
seen_lines = set()
|
|
102
102
|
all_content_parts = []
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
index = api.connect_core.send(cmd, target_list)
|
|
112
|
-
|
|
113
|
-
if index == 0:
|
|
114
|
-
# no waterball records
|
|
115
|
-
api.connect_core.send('q' + command.go_main_menu, [
|
|
116
|
-
connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
|
|
117
|
-
])
|
|
118
|
-
log.logger.info(i18n.get_waterball, '...', i18n.success)
|
|
119
|
-
return []
|
|
120
|
-
|
|
121
|
-
if index < 0:
|
|
122
|
-
break
|
|
123
|
-
|
|
124
|
-
screen = api.connect_core.get_screen_queue()[-1]
|
|
125
|
-
for line in screen.split('\n'):
|
|
126
|
-
line = line.strip()
|
|
127
|
-
if line and line not in seen_lines:
|
|
128
|
-
seen_lines.add(line)
|
|
129
|
-
all_content_parts.append(line)
|
|
130
|
-
|
|
131
|
-
if index == 1:
|
|
132
|
-
# WaterBallListEnd — collect done, exit viewer; handle post-action prompt
|
|
133
|
-
action_cmd = _post_action_cmd[post_action]
|
|
134
|
-
target_list_exit = [
|
|
135
|
-
connect_core.TargetUnit(screens.Target.WaterBallPostAction, log_level=log.DEBUG,
|
|
136
|
-
response=action_cmd + command.enter),
|
|
137
|
-
connect_core.TargetUnit(screens.Target.WaterballCleanup, log_level=log.DEBUG,
|
|
138
|
-
response='y' + command.enter),
|
|
139
|
-
connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
|
|
104
|
+
with _api_util.expanded_screen(api):
|
|
105
|
+
while True:
|
|
106
|
+
target_list = [
|
|
107
|
+
connect_core.TargetUnit('◆ 暫無訊息記錄', log_level=log.DEBUG, break_detect=True),
|
|
108
|
+
connect_core.TargetUnit(screens.Target.WaterBallListEnd, log_level=log.DEBUG, break_detect=True),
|
|
109
|
+
connect_core.TargetUnit(screens.Target.InWaterBallList, log_level=log.DEBUG, break_detect=True),
|
|
140
110
|
]
|
|
141
|
-
|
|
142
|
-
api.connect_core.send(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
111
|
+
|
|
112
|
+
index = api.connect_core.send(cmd, target_list)
|
|
113
|
+
|
|
114
|
+
if index == 0:
|
|
115
|
+
# no waterball records
|
|
116
|
+
api.connect_core.send('q' + command.go_main_menu, [
|
|
117
|
+
connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
|
|
118
|
+
])
|
|
119
|
+
log.logger.info(i18n.get_waterball, '...', i18n.success)
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
if index < 0:
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
screen = api.connect_core.get_screen_queue()[-1]
|
|
126
|
+
for line in screen.split('\n'):
|
|
127
|
+
line = line.strip()
|
|
128
|
+
if line and line not in seen_lines:
|
|
129
|
+
seen_lines.add(line)
|
|
130
|
+
all_content_parts.append(line)
|
|
131
|
+
|
|
132
|
+
if index == 1:
|
|
133
|
+
# WaterBallListEnd — collect done, exit viewer; handle post-action prompt
|
|
134
|
+
action_cmd = _post_action_cmd[post_action]
|
|
135
|
+
target_list_exit = [
|
|
136
|
+
connect_core.TargetUnit(screens.Target.WaterBallPostAction, log_level=log.DEBUG,
|
|
137
|
+
response=action_cmd + command.enter),
|
|
138
|
+
connect_core.TargetUnit(screens.Target.WaterballCleanup, log_level=log.DEBUG,
|
|
139
|
+
response='y' + command.enter),
|
|
140
|
+
connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
|
|
141
|
+
]
|
|
142
|
+
api.connect_core.send('q', target_list_exit)
|
|
143
|
+
api.connect_core.send(command.go_main_menu, [
|
|
144
|
+
connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
|
|
145
|
+
])
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
# index == 2: InWaterBallList — next page
|
|
149
|
+
cmd = command.ctrl_f
|
|
149
150
|
|
|
150
151
|
result = []
|
|
151
152
|
for line in _merge_continuation_lines(all_content_parts):
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from . import _api_util
|
|
4
|
+
from . import command
|
|
5
|
+
from . import connect_core
|
|
6
|
+
from . import log
|
|
7
|
+
from . import screens
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def has_new_mail(api) -> int:
|
|
11
|
+
cmd_list = []
|
|
12
|
+
cmd_list.append(command.go_main_menu)
|
|
13
|
+
cmd_list.append(command.ctrl_z)
|
|
14
|
+
cmd_list.append('m')
|
|
15
|
+
# cmd_list.append('1')
|
|
16
|
+
# cmd_list.append(command.enter)
|
|
17
|
+
cmd = ''.join(cmd_list)
|
|
18
|
+
current_capacity = None
|
|
19
|
+
plus_count = 0
|
|
20
|
+
index_pattern = re.compile(r'(\d+)')
|
|
21
|
+
checked_index_list = []
|
|
22
|
+
break_detect = False
|
|
23
|
+
|
|
24
|
+
target_list = [
|
|
25
|
+
connect_core.TargetUnit(screens.Target.InMailBox, log_level=log.DEBUG, break_detect=True)
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
api.connect_core.send(
|
|
29
|
+
cmd,
|
|
30
|
+
target_list,
|
|
31
|
+
)
|
|
32
|
+
current_capacity, _ = _api_util.get_mailbox_capacity(api)
|
|
33
|
+
if current_capacity > 20:
|
|
34
|
+
cmd_list = []
|
|
35
|
+
cmd_list.append(command.go_main_menu)
|
|
36
|
+
cmd_list.append(command.ctrl_z)
|
|
37
|
+
cmd_list.append('m')
|
|
38
|
+
cmd_list.append('1')
|
|
39
|
+
cmd_list.append(command.enter)
|
|
40
|
+
cmd = ''.join(cmd_list)
|
|
41
|
+
|
|
42
|
+
with _api_util.expanded_screen(api):
|
|
43
|
+
while True:
|
|
44
|
+
if current_capacity > 20:
|
|
45
|
+
api.connect_core.send(
|
|
46
|
+
cmd,
|
|
47
|
+
target_list,
|
|
48
|
+
)
|
|
49
|
+
last_screen = api.connect_core.get_screen_queue()[-1]
|
|
50
|
+
|
|
51
|
+
last_screen_list = last_screen.split('\n')
|
|
52
|
+
last_screen_list = last_screen_list[3:-1]
|
|
53
|
+
last_screen_list = [x[:10] for x in last_screen_list]
|
|
54
|
+
|
|
55
|
+
current_plus_count = 0
|
|
56
|
+
for line in last_screen_list:
|
|
57
|
+
if str(current_capacity) in line:
|
|
58
|
+
break_detect = True
|
|
59
|
+
|
|
60
|
+
index_result = index_pattern.search(line)
|
|
61
|
+
if index_result is None:
|
|
62
|
+
continue
|
|
63
|
+
current_index = index_result.group(0)
|
|
64
|
+
if current_index in checked_index_list:
|
|
65
|
+
continue
|
|
66
|
+
checked_index_list.append(current_index)
|
|
67
|
+
if '+' not in line:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
current_plus_count += 1
|
|
71
|
+
|
|
72
|
+
plus_count += current_plus_count
|
|
73
|
+
if break_detect:
|
|
74
|
+
break
|
|
75
|
+
cmd = command.ctrl_f
|
|
76
|
+
|
|
77
|
+
return plus_count
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import threading
|
|
5
|
+
from contextlib import contextmanager
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from typing import Dict, Optional
|
|
7
8
|
|
|
@@ -14,6 +15,27 @@ from . import log
|
|
|
14
15
|
from . import screens
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
_MAX_GET_CONTENT_HEIGHT = 100 # PTT server hard cap
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@contextmanager
|
|
22
|
+
def expanded_screen(api):
|
|
23
|
+
"""Temporarily resize the PTT terminal to its maximum (100 rows) and restore on exit.
|
|
24
|
+
|
|
25
|
+
Wraps any paging loop that calls connect_core.send() in a loop so that more
|
|
26
|
+
content is visible per page and fewer round-trips are needed. PTT's hard
|
|
27
|
+
cap is 100 rows regardless of what NAWS requests.
|
|
28
|
+
"""
|
|
29
|
+
original_height = api.config.screen_height
|
|
30
|
+
if original_height < _MAX_GET_CONTENT_HEIGHT:
|
|
31
|
+
api.connect_core.set_screen_height(_MAX_GET_CONTENT_HEIGHT)
|
|
32
|
+
try:
|
|
33
|
+
yield
|
|
34
|
+
finally:
|
|
35
|
+
if original_height < _MAX_GET_CONTENT_HEIGHT:
|
|
36
|
+
api.connect_core.set_screen_height(original_height)
|
|
37
|
+
|
|
38
|
+
|
|
17
39
|
@dataclass
|
|
18
40
|
class PostQueryResult:
|
|
19
41
|
lock_post: bool
|
|
@@ -30,158 +52,167 @@ class PostQueryResult:
|
|
|
30
52
|
def get_content(api, post_mode: bool = True):
|
|
31
53
|
api.Unconfirmed = False
|
|
32
54
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if post_mode:
|
|
37
|
-
cmd = command.enter * 2
|
|
38
|
-
else:
|
|
39
|
-
cmd = command.enter
|
|
40
|
-
|
|
41
|
-
target_list = [
|
|
42
|
-
# 待證實文章
|
|
43
|
-
connect_core.TargetUnit('本篇文章內容經站方授權之板務管理人員判斷有尚待證實之處', response=' ',
|
|
44
|
-
handler=is_unconfirmed_handler),
|
|
45
|
-
connect_core.TargetUnit(screens.Target.PostEnd, log_level=log.DEBUG, break_detect=True),
|
|
46
|
-
connect_core.TargetUnit(screens.Target.InPost, log_level=log.DEBUG, break_detect=True),
|
|
47
|
-
connect_core.TargetUnit(screens.Target.PostNoContent, log_level=log.DEBUG, break_detect=True),
|
|
48
|
-
# 動畫文章
|
|
49
|
-
connect_core.TargetUnit(screens.Target.Animation, response='n'),
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
line_from_pattern = re.compile(r'[\d]+~[\d]+')
|
|
53
|
-
|
|
54
|
-
has_control_code = False
|
|
55
|
-
control_code_mode = False
|
|
56
|
-
push_start = False
|
|
57
|
-
content_start_exist = False
|
|
58
|
-
content_start_jump = False
|
|
59
|
-
content_start_jump_set = False
|
|
60
|
-
|
|
61
|
-
first_page = True
|
|
62
|
-
origin_post = []
|
|
63
|
-
stop_dict = dict()
|
|
64
|
-
new_content_part = ''
|
|
65
|
-
get_line_b = 0
|
|
66
|
-
|
|
67
|
-
while True:
|
|
68
|
-
index = api.connect_core.send(cmd, target_list)
|
|
69
|
-
if index == 3 or index == 4:
|
|
70
|
-
return None, False
|
|
71
|
-
|
|
72
|
-
last_screen = api.connect_core.get_screen_queue()[-1]
|
|
73
|
-
lines = last_screen.split('\n')
|
|
74
|
-
last_line = lines[-1]
|
|
75
|
-
lines.pop()
|
|
76
|
-
last_screen = '\n'.join(lines)
|
|
77
|
-
|
|
78
|
-
if screens.Target.content_start in last_screen and not content_start_exist:
|
|
79
|
-
content_start_exist = True
|
|
80
|
-
|
|
81
|
-
if content_start_exist:
|
|
82
|
-
if not content_start_jump_set:
|
|
83
|
-
if screens.Target.content_start not in last_screen:
|
|
84
|
-
content_start_jump = True
|
|
85
|
-
content_start_jump_set = True
|
|
86
|
-
else:
|
|
87
|
-
content_start_jump = False
|
|
55
|
+
with expanded_screen(api):
|
|
56
|
+
def is_unconfirmed_handler(screen):
|
|
57
|
+
api.Unconfirmed = True
|
|
88
58
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
control_code_mode = True
|
|
92
|
-
has_control_code = True
|
|
93
|
-
else:
|
|
94
|
-
last_read_line_list = pattern_result.group(0).split('~')
|
|
95
|
-
last_read_line_a_temp = int(last_read_line_list[0])
|
|
96
|
-
last_read_line_b_temp = int(last_read_line_list[1])
|
|
97
|
-
if control_code_mode:
|
|
98
|
-
last_read_line_a = last_read_line_a_temp - 1
|
|
99
|
-
last_read_line_b = last_read_line_b_temp - 1
|
|
100
|
-
control_code_mode = False
|
|
101
|
-
|
|
102
|
-
if first_page:
|
|
103
|
-
first_page = False
|
|
104
|
-
origin_post.append(last_screen)
|
|
59
|
+
if post_mode:
|
|
60
|
+
cmd = command.enter * 2
|
|
105
61
|
else:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
# print(f'last_read_line_b {last_read_line_b}')
|
|
119
|
-
# print(f'GetLineB {last_read_line_a_temp - last_read_line_a}')
|
|
120
|
-
# print(f'GetLineA {last_read_line_b_temp - last_read_line_b}')
|
|
121
|
-
# print(f'show line {last_read_line_b_temp - last_read_line_a_temp + 1}')
|
|
122
|
-
if not control_code_mode:
|
|
62
|
+
cmd = command.enter
|
|
63
|
+
|
|
64
|
+
target_list = [
|
|
65
|
+
# 待證實文章
|
|
66
|
+
connect_core.TargetUnit('本篇文章內容經站方授權之板務管理人員判斷有尚待證實之處', response=' ',
|
|
67
|
+
handler=is_unconfirmed_handler),
|
|
68
|
+
connect_core.TargetUnit(screens.Target.PostEnd, log_level=log.DEBUG, break_detect=True),
|
|
69
|
+
connect_core.TargetUnit(screens.Target.InPost, log_level=log.DEBUG, break_detect=True),
|
|
70
|
+
connect_core.TargetUnit(screens.Target.PostNoContent, log_level=log.DEBUG, break_detect=True),
|
|
71
|
+
# 動畫文章
|
|
72
|
+
connect_core.TargetUnit(screens.Target.Animation, response='n'),
|
|
73
|
+
]
|
|
123
74
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
75
|
+
line_from_pattern = re.compile(r'[\d]+~[\d]+')
|
|
76
|
+
|
|
77
|
+
has_control_code = False
|
|
78
|
+
control_code_mode = False
|
|
79
|
+
push_start = False
|
|
80
|
+
content_start_exist = False
|
|
81
|
+
content_start_jump = False
|
|
82
|
+
content_start_jump_set = False
|
|
83
|
+
|
|
84
|
+
first_page = True
|
|
85
|
+
origin_post = []
|
|
86
|
+
stop_dict = dict()
|
|
87
|
+
new_content_part = ''
|
|
88
|
+
get_line_b = 0
|
|
89
|
+
|
|
90
|
+
while True:
|
|
91
|
+
index = api.connect_core.send(cmd, target_list)
|
|
92
|
+
if index == 3 or index == 4:
|
|
93
|
+
return None, False
|
|
94
|
+
|
|
95
|
+
last_screen = api.connect_core.get_screen_queue()[-1]
|
|
96
|
+
lines = last_screen.split('\n')
|
|
97
|
+
last_line = lines[-1]
|
|
98
|
+
lines.pop()
|
|
99
|
+
last_screen = '\n'.join(lines)
|
|
100
|
+
|
|
101
|
+
if screens.Target.content_start in last_screen and not content_start_exist:
|
|
102
|
+
content_start_exist = True
|
|
103
|
+
|
|
104
|
+
if content_start_exist:
|
|
105
|
+
if not content_start_jump_set:
|
|
106
|
+
if screens.Target.content_start not in last_screen:
|
|
107
|
+
content_start_jump = True
|
|
108
|
+
content_start_jump_set = True
|
|
128
109
|
else:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
110
|
+
content_start_jump = False
|
|
111
|
+
|
|
112
|
+
pattern_result = line_from_pattern.search(last_line)
|
|
113
|
+
if pattern_result is None:
|
|
114
|
+
control_code_mode = True
|
|
115
|
+
has_control_code = True
|
|
116
|
+
else:
|
|
117
|
+
last_read_line_list = pattern_result.group(0).split('~')
|
|
118
|
+
last_read_line_a_temp = int(last_read_line_list[0])
|
|
119
|
+
last_read_line_b_temp = int(last_read_line_list[1])
|
|
120
|
+
if control_code_mode:
|
|
121
|
+
last_read_line_a = last_read_line_a_temp - 1
|
|
122
|
+
last_read_line_b = last_read_line_b_temp - 1
|
|
123
|
+
control_code_mode = False
|
|
124
|
+
|
|
125
|
+
if first_page:
|
|
126
|
+
first_page = False
|
|
127
|
+
origin_post.append(last_screen)
|
|
128
|
+
else:
|
|
129
|
+
# 這裡是根據觀察畫面行數的變化歸納出的神奇公式...
|
|
130
|
+
# 輸出的結果是要判斷出畫面的最後 x 行是新的文章內容
|
|
131
|
+
#
|
|
132
|
+
# 這裡是 PyPtt 最黑暗最墮落的地方,所有你所知的程式碼守則,在這裡都不適用
|
|
133
|
+
# 每除完一次錯誤,我會陷入嚴重的創傷後壓力症候群,而我的腦袋會自動選擇遺忘這裡所有的一切
|
|
134
|
+
# 以確保下一個週一,我可以正常上班
|
|
135
|
+
# but it works!!!
|
|
136
|
+
|
|
137
|
+
# print(LastScreen)
|
|
138
|
+
# print(f'last_read_line_a_temp [{last_read_line_a_temp}]')
|
|
139
|
+
# print(f'last_read_line_b_temp [{last_read_line_b_temp}]')
|
|
140
|
+
# print(f'last_read_line_a {last_read_line_a}')
|
|
141
|
+
# print(f'last_read_line_b {last_read_line_b}')
|
|
142
|
+
# print(f'GetLineB {last_read_line_a_temp - last_read_line_a}')
|
|
143
|
+
# print(f'GetLineA {last_read_line_b_temp - last_read_line_b}')
|
|
144
|
+
# print(f'show line {last_read_line_b_temp - last_read_line_a_temp + 1}')
|
|
145
|
+
if not control_code_mode:
|
|
146
|
+
|
|
147
|
+
if last_read_line_a_temp in stop_dict:
|
|
148
|
+
new_content_part = '\n'.join(
|
|
149
|
+
lines[-stop_dict[last_read_line_a_temp]:])
|
|
141
150
|
stop_dict = dict()
|
|
142
151
|
else:
|
|
143
|
-
|
|
144
|
-
if
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
#
|
|
152
|
-
|
|
152
|
+
get_line_b = last_read_line_b_temp - last_read_line_b
|
|
153
|
+
if get_line_b > 0:
|
|
154
|
+
# print('Type 1')
|
|
155
|
+
new_content_part = '\n'.join(lines[-get_line_b:])
|
|
156
|
+
# The doubling and try_line heuristics were tuned for
|
|
157
|
+
# 24-row screens (max get_line_b ≈ 22). With larger
|
|
158
|
+
# screen_height the delta can be 50+, causing both
|
|
159
|
+
# heuristics to look back into the post header rows
|
|
160
|
+
# (作者/標題/時間) and inject them into the body.
|
|
161
|
+
# Only apply them when the delta is within the original
|
|
162
|
+
# 24-row regime.
|
|
163
|
+
if get_line_b <= 23:
|
|
164
|
+
if index == 1 and len(new_content_part) == get_line_b - 1:
|
|
165
|
+
new_content_part = '\n'.join(lines[-(get_line_b * 2):])
|
|
166
|
+
elif origin_post and get_line_b + 1 <= len(lines):
|
|
167
|
+
last_line_temp = origin_post[-1].strip()
|
|
168
|
+
try_line = lines[-(get_line_b + 1)].strip()
|
|
169
|
+
|
|
170
|
+
if not last_line_temp.endswith(try_line):
|
|
171
|
+
new_content_part = try_line + '\n' + new_content_part
|
|
172
|
+
stop_dict = dict()
|
|
153
173
|
else:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
174
|
+
# 駐足現象,LastReadLineB跟上一次相比並沒有改變
|
|
175
|
+
if (last_read_line_b_temp + 1) not in stop_dict:
|
|
176
|
+
stop_dict[last_read_line_b_temp + 1] = 1
|
|
177
|
+
stop_dict[last_read_line_b_temp + 1] += 1
|
|
158
178
|
|
|
159
|
-
|
|
179
|
+
get_line_a = last_read_line_a_temp - last_read_line_a
|
|
160
180
|
|
|
161
|
-
|
|
181
|
+
if get_line_a > 0:
|
|
182
|
+
# print(f'Type 2 get_line_a [{get_line_a}]')
|
|
183
|
+
new_content_part = '\n'.join(lines[-get_line_a:])
|
|
184
|
+
else:
|
|
185
|
+
new_content_part = '\n'.join(lines)
|
|
162
186
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
get_line_b += 1
|
|
166
|
-
new_content_part = '\n'.join(lines[-get_line_b:])
|
|
187
|
+
else:
|
|
188
|
+
new_content_part = lines[-1]
|
|
167
189
|
|
|
168
|
-
origin_post.pop()
|
|
169
190
|
origin_post.append(new_content_part)
|
|
170
|
-
break
|
|
171
191
|
|
|
172
|
-
|
|
173
|
-
last_read_line_a = last_read_line_a_temp
|
|
174
|
-
last_read_line_b = last_read_line_b_temp
|
|
192
|
+
log.logger.debug('NewContentPart', new_content_part)
|
|
175
193
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
194
|
+
if index == 1:
|
|
195
|
+
if content_start_jump and len(new_content_part) == 0:
|
|
196
|
+
get_line_b += 1
|
|
197
|
+
new_content_part = '\n'.join(lines[-get_line_b:])
|
|
198
|
+
|
|
199
|
+
origin_post.pop()
|
|
200
|
+
origin_post.append(new_content_part)
|
|
179
201
|
break
|
|
180
202
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
203
|
+
if not control_code_mode:
|
|
204
|
+
last_read_line_a = last_read_line_a_temp
|
|
205
|
+
last_read_line_b = last_read_line_b_temp
|
|
206
|
+
|
|
207
|
+
for EC in screens.Target.content_end_list:
|
|
208
|
+
if EC in last_screen:
|
|
209
|
+
push_start = True
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
if push_start:
|
|
213
|
+
cmd = command.right
|
|
214
|
+
else:
|
|
215
|
+
cmd = command.down
|
|
185
216
|
|
|
186
217
|
# print(api.Unconfirmed)
|
|
187
218
|
origin_post = '\n'.join(origin_post)
|