PyPtt 2.1.1__tar.gz → 2.1.3__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.
Files changed (90) hide show
  1. {pyptt-2.1.1 → pyptt-2.1.3}/PKG-INFO +1 -1
  2. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/PTT.py +9 -2
  3. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/__init__.py +1 -1
  4. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_del_post.py +41 -12
  5. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_get_board_list.py +4 -0
  6. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_get_favourite_board.py +1 -0
  7. pyptt-2.1.3/PyPtt/_api_get_post_list.py +111 -0
  8. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_get_waterball.py +45 -44
  9. pyptt-2.1.3/PyPtt/_api_has_new_mail.py +77 -0
  10. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_util.py +167 -136
  11. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/config.py +5 -0
  12. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/connect_core.py +23 -2
  13. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/screens.py +2 -2
  14. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/ssl_config.py +9 -9
  15. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt.egg-info/PKG-INFO +1 -1
  16. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt.egg-info/SOURCES.txt +1 -0
  17. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_del_post.py +62 -0
  18. pyptt-2.1.3/tests/test_get_content_formula.py +431 -0
  19. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_post_list_parser.py +54 -0
  20. pyptt-2.1.1/PyPtt/_api_get_post_list.py +0 -104
  21. pyptt-2.1.1/PyPtt/_api_has_new_mail.py +0 -76
  22. {pyptt-2.1.1 → pyptt-2.1.3}/LICENSE +0 -0
  23. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_bucket.py +0 -0
  24. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_call_status.py +0 -0
  25. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_change_pw.py +0 -0
  26. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_comment.py +0 -0
  27. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_get_board_info.py +0 -0
  28. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_get_bottom_post_list.py +0 -0
  29. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_get_newest_index.py +0 -0
  30. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_get_post.py +0 -0
  31. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_get_post_index.py +0 -0
  32. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_get_time.py +0 -0
  33. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_get_user.py +0 -0
  34. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_give_money.py +0 -0
  35. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_loginout.py +0 -0
  36. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_mail.py +0 -0
  37. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_mark_post.py +0 -0
  38. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_post.py +0 -0
  39. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_reply_post.py +0 -0
  40. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_search_user.py +0 -0
  41. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/_api_set_board_title.py +0 -0
  42. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/api_server.py +0 -0
  43. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/check_value.py +0 -0
  44. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/command.py +0 -0
  45. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/data_type.py +0 -0
  46. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/exceptions.py +0 -0
  47. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/i18n.py +0 -0
  48. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/lang_en_US.py +0 -0
  49. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/lang_zh_TW.py +0 -0
  50. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/lib_util.py +0 -0
  51. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/log.py +0 -0
  52. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt/service.py +0 -0
  53. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt.egg-info/dependency_links.txt +0 -0
  54. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt.egg-info/requires.txt +0 -0
  55. {pyptt-2.1.1 → pyptt-2.1.3}/PyPtt.egg-info/top_level.txt +0 -0
  56. {pyptt-2.1.1 → pyptt-2.1.3}/README.md +0 -0
  57. {pyptt-2.1.1 → pyptt-2.1.3}/pyproject.toml +0 -0
  58. {pyptt-2.1.1 → pyptt-2.1.3}/setup.cfg +0 -0
  59. {pyptt-2.1.1 → pyptt-2.1.3}/setup.py +0 -0
  60. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_bucket.py +0 -0
  61. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_change_pw.py +0 -0
  62. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_comment.py +0 -0
  63. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_exceptions.py +0 -0
  64. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_fast_post.py +0 -0
  65. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_board_info.py +0 -0
  66. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_board_list.py +0 -0
  67. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_bottom_post_list.py +0 -0
  68. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_favourite_boards.py +0 -0
  69. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_mail.py +0 -0
  70. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_newest_index.py +0 -0
  71. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_post.py +0 -0
  72. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_post_list.py +0 -0
  73. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_post_parser.py +0 -0
  74. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_time.py +0 -0
  75. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_user.py +0 -0
  76. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_get_waterball.py +0 -0
  77. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_give_money.py +0 -0
  78. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_i18n.py +0 -0
  79. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_init.py +0 -0
  80. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_logger.py +0 -0
  81. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_mail_send_and_del.py +0 -0
  82. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_mark_post.py +0 -0
  83. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_parse_query_post.py +0 -0
  84. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_post.py +0 -0
  85. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_pyte_parity.py +0 -0
  86. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_reply_post.py +0 -0
  87. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_search_user.py +0 -0
  88. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_service.py +0 -0
  89. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_set_board_title.py +0 -0
  90. {pyptt-2.1.1 → pyptt-2.1.3}/tests/test_vt100_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: PyPtt github: https://github.com/PyPtt/PyPtt
5
5
  Author-email: CodingMan <pttcodingman@gmail.com>
6
6
  License: GNU Lesser General Public License v3 (LGPLv3)
@@ -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()
@@ -1066,7 +1071,7 @@ class API:
1066
1071
 
1067
1072
  return _api_get_bottom_post_list.get_bottom_post_list(self, board)
1068
1073
 
1069
- def del_post(self, board: str, aid: Optional[str] = None, index: int = 0) -> None:
1074
+ def del_post(self, board: str, aid: Optional[str] = None, index: int = 0, reason: Optional[str] = None) -> None:
1070
1075
  """
1071
1076
  刪除文章。
1072
1077
 
@@ -1074,6 +1079,7 @@ class API:
1074
1079
  board (str): 看板名稱。
1075
1080
  aid (str): 文章編號。
1076
1081
  index (int): 文章編號。
1082
+ reason (str): 板主刪除他人文章時,加註於刪除後標題的理由(僅板主刪除他板友文章時有效)。
1077
1083
 
1078
1084
  Returns:
1079
1085
  None
@@ -1084,6 +1090,7 @@ class API:
1084
1090
  NoSuchBoard: 看板不存在。
1085
1091
  NoSuchPost: 文章不存在。
1086
1092
  NoPermission: 沒有權限。
1093
+ ParameterError: 對自己的文章傳入 reason(加註理由僅適用於版主刪除他人文章)。
1087
1094
 
1088
1095
  範例::
1089
1096
 
@@ -1098,7 +1105,7 @@ class API:
1098
1105
  ptt_bot.logout()
1099
1106
  """
1100
1107
 
1101
- _api_del_post.del_post(self, board, aid, index)
1108
+ _api_del_post.del_post(self, board, aid, index, reason)
1102
1109
 
1103
1110
  def get_post_list(self, board: str, limit: int = 20, offset: int = 0) -> list[dict]:
1104
1111
  """
@@ -1,4 +1,4 @@
1
- __version__ = '2.1.1'
1
+ __version__ = '2.1.3'
2
2
  __author__ = 'CodingMan'
3
3
  __email__ = 'pttcodingman@gmail.com'
4
4
 
@@ -14,7 +14,7 @@ from . import log
14
14
  from . import screens
15
15
 
16
16
 
17
- def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0) -> None:
17
+ def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int = 0, reason: Optional[str] = None) -> None:
18
18
  _api_util.one_thread(api)
19
19
 
20
20
  if not api.is_registered_user:
@@ -27,6 +27,8 @@ def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int =
27
27
  if post_aid is not None:
28
28
  check_value.check_type(post_aid, str, 'PostAID')
29
29
  check_value.check_type(post_index, int, 'PostIndex')
30
+ if reason is not None:
31
+ check_value.check_type(reason, str, 'reason')
30
32
 
31
33
  if len(board) == 0:
32
34
  raise exceptions.ParameterError(f'board error parameter: {board}')
@@ -64,6 +66,13 @@ def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int =
64
66
  log.logger.info(i18n.delete_post, '...', i18n.success)
65
67
  return
66
68
 
69
+ # PTT only offers the "R加註理由" annotation when a moderator deletes
70
+ # another user's post. On your own post the reason can never be applied,
71
+ # so reject it rather than silently dropping it.
72
+ if reason and api.ptt_id.lower() == post_info[data_type.PostField.author].lower():
73
+ raise exceptions.ParameterError(
74
+ 'reason is only valid when a moderator deletes another user\'s post')
75
+
67
76
  if check_author:
68
77
  if api.ptt_id.lower() != post_info[data_type.PostField.author].lower():
69
78
  log.logger.info(i18n.delete_post, '...', i18n.fail)
@@ -90,20 +99,40 @@ def del_post(api, board: str, post_aid: Optional[str] = None, post_index: int =
90
99
  def confirm_delete_handler(screen):
91
100
  api.confirm = True
92
101
 
102
+ # A board moderator deleting another user's post gets an extra
103
+ # "R加註理由" option that lets us annotate the title shown after deletion.
104
+ use_reason = bool(reason) and not check_author
105
+
106
+ def confirm_delete_response(screen):
107
+ if use_reason and '加註理由' in screen:
108
+ return 'r' + command.enter
109
+ return 'y' + command.enter
110
+
93
111
  target_list = [
94
112
  connect_core.TargetUnit('請按任意鍵繼續', response=' '),
95
- connect_core.TargetUnit('請確定刪除(Y/N)?[N]', response='y' + command.enter, handler=confirm_delete_handler,
96
- max_match=1),
97
- connect_core.TargetUnit(screens.Target.InBoard, break_detect=True),
113
+ connect_core.TargetUnit('請確定刪除(Y/N', response=confirm_delete_response,
114
+ handler=confirm_delete_handler, max_match=1),
98
115
  ]
99
116
 
100
- index = api.connect_core.send(
101
- cmd,
102
- target_list)
103
-
104
- if index == 1:
105
- if not api.confirm:
106
- log.logger.info(i18n.delete_post, '...', i18n.fail)
107
- raise exceptions.NoPermission(i18n.no_permission)
117
+ if use_reason:
118
+ target_list.append(
119
+ connect_core.TargetUnit(
120
+ '請輸入刪除後要顯示的標題',
121
+ response=command.ctrl_e + ' ' + reason + command.enter,
122
+ max_match=1))
123
+ target_list.append(
124
+ connect_core.TargetUnit(
125
+ '請再次確定是否要用上述理由刪除(Y/N',
126
+ response='y' + command.enter,
127
+ max_match=1))
128
+
129
+ target_list.append(
130
+ connect_core.TargetUnit(screens.Target.InBoard, break_detect=True))
131
+
132
+ api.connect_core.send(cmd, target_list)
133
+
134
+ if not api.confirm:
135
+ log.logger.info(i18n.delete_post, '...', i18n.fail)
136
+ raise exceptions.NoPermission(i18n.no_permission)
108
137
 
109
138
  log.logger.info(i18n.delete_post, '...', i18n.success)
@@ -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(
@@ -24,6 +24,7 @@ def get_favourite_board(api) -> list:
24
24
 
25
25
  board_list = []
26
26
  favourite_board_list = []
27
+ # NOTE: expanded_screen intentionally not used — same risk as get_board_list.
27
28
  while True:
28
29
 
29
30
  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
- while True:
105
- target_list = [
106
- connect_core.TargetUnit('◆ 暫無訊息記錄', log_level=log.DEBUG, break_detect=True),
107
- connect_core.TargetUnit(screens.Target.WaterBallListEnd, log_level=log.DEBUG, break_detect=True),
108
- connect_core.TargetUnit(screens.Target.InWaterBallList, log_level=log.DEBUG, break_detect=True),
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
- api.connect_core.send('q', target_list_exit)
142
- api.connect_core.send(command.go_main_menu, [
143
- connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
144
- ])
145
- break
146
-
147
- # index == 2: InWaterBallList — next page
148
- cmd = command.ctrl_f
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