PyPtt 2.3.10__tar.gz → 2.3.12__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.3.10 → pyptt-2.3.12}/PKG-INFO +1 -1
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/PTT.py +1 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/__init__.py +1 -1
- pyptt-2.3.12/PyPtt/_api_bucket.py +80 -0
- pyptt-2.3.12/PyPtt/_api_get_board_info.py +325 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_get_favourite_board.py +18 -33
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_get_newest_index.py +6 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_get_post_list.py +3 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_give_money.py +8 -4
- pyptt-2.3.12/PyPtt/_api_set_board_title.py +153 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_util.py +131 -1
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/connect_core.py +74 -19
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/screens.py +83 -7
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt.egg-info/PKG-INFO +1 -1
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt.egg-info/SOURCES.txt +3 -0
- pyptt-2.3.12/tests/test_board_setting_flow.py +468 -0
- pyptt-2.3.12/tests/test_get_board_info_parser.py +314 -0
- pyptt-2.3.12/tests/test_new_screen_fixes.py +406 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_set_board_title.py +5 -1
- pyptt-2.3.10/PyPtt/_api_bucket.py +0 -62
- pyptt-2.3.10/PyPtt/_api_get_board_info.py +0 -278
- pyptt-2.3.10/PyPtt/_api_set_board_title.py +0 -75
- {pyptt-2.3.10 → pyptt-2.3.12}/LICENSE +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_call_status.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_change_pw.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_comment.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_del_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_get_board_list.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_get_bottom_post_list.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_get_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_get_post_index.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_get_time.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_get_user.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_get_waterball.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_has_new_mail.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_loginout.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_mail.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_mark_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_reply_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_search_user.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_api_set_signature_file.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_uao/LICENSE +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_uao/__init__.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_uao/b2u.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/_uao/u2b.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/api_server.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/check_value.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/command.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/config.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/data_type.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/exceptions.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/i18n.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/lang_en_US.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/lang_zh_TW.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/lib_util.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/log.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/service.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt/ssl_config.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt.egg-info/dependency_links.txt +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt.egg-info/requires.txt +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/PyPtt.egg-info/top_level.txt +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/README.md +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/pyproject.toml +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/setup.cfg +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/setup.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_backup_effective.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_backup_param.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_bucket.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_change_pw.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_comment.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_connect_core_timeout.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_del_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_del_post_unit.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_exceptions.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_fast_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_board_info.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_board_list.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_bottom_post_list.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_content_formula.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_favourite_boards.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_mail.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_newest_index.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_post_list.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_post_list_parser.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_post_parser.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_time.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_user.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_get_waterball.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_give_money.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_half_dead_connection.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_i18n.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_incremental_parity.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_init.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_logger.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_login_logout.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_mail_send_and_del.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_main_menu_status_bar.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_mark_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_parse_query_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_post_anonymous.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_pyte_parity.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_reply_post.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_search_user.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_service.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_set_signature_file.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_status_line.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_uao_codec.py +0 -0
- {pyptt-2.3.10 → pyptt-2.3.12}/tests/test_vt100_parser.py +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from . import _api_set_board_title
|
|
4
|
+
from . import _api_util
|
|
5
|
+
from . import check_value
|
|
6
|
+
from . import command
|
|
7
|
+
from . import exceptions
|
|
8
|
+
from . import i18n
|
|
9
|
+
from . import lib_util
|
|
10
|
+
from . import screens
|
|
11
|
+
from ._api_set_board_title import step
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _bucket_days_max() -> int:
|
|
15
|
+
# pttbbs mbbsd/acl.c:180-206: 期限輸入欄 getdata(..., datebuf, 8) 最多 7 位數,
|
|
16
|
+
# 到期時間用 time4_add_sec() 算,超過 0xFFFFFFFF (2106-02-07) 就回「日期過大」
|
|
17
|
+
# (include/cmsys.h:86-93)。所以上限是「現在到 2106-02-07」的整天數。
|
|
18
|
+
return min(9999999, (0xFFFFFFFF - int(time.time())) // 86400 - 1)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def bucket(api, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:
|
|
22
|
+
_api_util.one_thread(api)
|
|
23
|
+
|
|
24
|
+
if not api._is_login:
|
|
25
|
+
raise exceptions.RequireLogin(i18n.require_login)
|
|
26
|
+
|
|
27
|
+
if not api.is_registered_user:
|
|
28
|
+
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
|
|
29
|
+
|
|
30
|
+
check_value.check_type(board, str, 'board')
|
|
31
|
+
check_value.check_type(bucket_days, int, 'bucket_days')
|
|
32
|
+
check_value.check_type(reason, str, 'reason')
|
|
33
|
+
check_value.check_type(ptt_id, str, 'ptt_id')
|
|
34
|
+
# bool 是 int 的子類別;期限要是正整數天,且不超過 PTT 能存的到期時間
|
|
35
|
+
max_days = _bucket_days_max()
|
|
36
|
+
if isinstance(bucket_days, bool) or not 0 < bucket_days <= max_days:
|
|
37
|
+
raise exceptions.ParameterError(f'bucket_days must be an int in 1..{max_days}, got {bucket_days!r}')
|
|
38
|
+
_api_util.check_no_control_chars(reason, 'reason')
|
|
39
|
+
_api_util.check_no_control_chars(ptt_id, 'ptt_id')
|
|
40
|
+
|
|
41
|
+
_api_util.check_user_exist(api, ptt_id)
|
|
42
|
+
|
|
43
|
+
_api_util.check_board(api, board, check_moderator=True)
|
|
44
|
+
|
|
45
|
+
_api_util.goto_board(api, board)
|
|
46
|
+
|
|
47
|
+
# 從這裡起進入看板設定;任何結束路徑都由 run_in_board_setting 退回文章列表
|
|
48
|
+
_api_util.run_in_board_setting(api, lambda: _bucket_in_board_setting(api, board, bucket_days, reason, ptt_id))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _bucket_in_board_setting(api, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:
|
|
52
|
+
|
|
53
|
+
# 新版編輯模式裡 w 直接打開 Bakuman (不能再補 Enter,Enter 會被當成 [Q]結束);
|
|
54
|
+
# 舊版 w 是「請輸入要改變的設定」單行輸入,要 Enter 送出。
|
|
55
|
+
mode = _api_set_board_title.open_board_setting(api, board, 'i')
|
|
56
|
+
step(api, 'w' if mode == 'new' else 'w' + command.enter, [('bakuman', screens.Target.Bakuman)])
|
|
57
|
+
|
|
58
|
+
# Bakuman 新舊版相同,每一步都比對提示再送下一步
|
|
59
|
+
step(api, 'a' + command.enter, [('id', '請輸入使用者代號')])
|
|
60
|
+
result = step(api, ptt_id + command.enter, [
|
|
61
|
+
('period', '請以數字跟單位(預設為天)輸入期限'),
|
|
62
|
+
('banned', '◆ 使用者之前已被禁言'),
|
|
63
|
+
])
|
|
64
|
+
if result == 'banned':
|
|
65
|
+
raise exceptions.UserHasPreviouslyBeenBanned()
|
|
66
|
+
|
|
67
|
+
step(api, str(bucket_days) + command.enter, [('reason', '請輸入理由')])
|
|
68
|
+
step(api, reason + command.enter, [('confirm', '確認以上資料全部正確嗎')])
|
|
69
|
+
step(api, 'y' + command.enter, [
|
|
70
|
+
('done', '◆ 已將使用者加入禁言名單'),
|
|
71
|
+
], screen_timeout=api.config.screen_long_timeout)
|
|
72
|
+
|
|
73
|
+
# [按任意鍵繼續] → 回到 [Q]結束? → Enter (空白 = Q) 離開 Bakuman
|
|
74
|
+
step(api, command.left, [('bakuman', screens.Target.Bakuman)])
|
|
75
|
+
if mode == 'new':
|
|
76
|
+
step(api, command.enter, [('edit', screens.Target.BoardSettingEdit)])
|
|
77
|
+
else:
|
|
78
|
+
step(api, command.enter, [('prompt', screens.Target.BoardSettingOldPrompt)])
|
|
79
|
+
|
|
80
|
+
_api_set_board_title.close_board_setting(api, mode)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from . import _api_util
|
|
5
|
+
from . import check_value
|
|
6
|
+
from . import command
|
|
7
|
+
from . import connect_core
|
|
8
|
+
from . import exceptions
|
|
9
|
+
from . import i18n
|
|
10
|
+
from . import log
|
|
11
|
+
from . import screens
|
|
12
|
+
from .data_type import BoardField
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -> Dict:
|
|
16
|
+
logger = log.init(log.DEBUG if call_by_others else log.INFO)
|
|
17
|
+
|
|
18
|
+
_api_util.one_thread(api)
|
|
19
|
+
|
|
20
|
+
if not api._is_login:
|
|
21
|
+
raise exceptions.RequireLogin(i18n.require_login)
|
|
22
|
+
|
|
23
|
+
check_value.check_type(board, str, 'board')
|
|
24
|
+
|
|
25
|
+
logger.info(
|
|
26
|
+
i18n.replace(i18n.get_board_info, board))
|
|
27
|
+
|
|
28
|
+
_api_util.goto_board(api, board, refresh=True)
|
|
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.
|
|
35
|
+
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
36
|
+
# print(ori_screen)
|
|
37
|
+
|
|
38
|
+
online_user_line = None
|
|
39
|
+
for line in ori_screen.split('\n'):
|
|
40
|
+
if '作 者' not in line:
|
|
41
|
+
continue
|
|
42
|
+
if '日 期' not in line:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
online_user_line = line
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
if online_user_line is None:
|
|
49
|
+
if api.connect_core.last_timeout_was_silent:
|
|
50
|
+
raise exceptions.ConnectionClosed()
|
|
51
|
+
raise exceptions.NoSuchBoard(api.config, board)
|
|
52
|
+
|
|
53
|
+
if '[靜]' in online_user_line:
|
|
54
|
+
online_user = 0
|
|
55
|
+
else:
|
|
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()
|
|
59
|
+
raise exceptions.NoSuchBoard(api.config, board)
|
|
60
|
+
pattern = re.compile(r'[\d]+')
|
|
61
|
+
r = pattern.search(online_user_line)
|
|
62
|
+
if r is None:
|
|
63
|
+
if api.connect_core.last_timeout_was_silent:
|
|
64
|
+
raise exceptions.ConnectionClosed()
|
|
65
|
+
raise exceptions.NoSuchBoard(api.config, board)
|
|
66
|
+
# 減一是把自己本身拿掉
|
|
67
|
+
online_user = int(r.group(0)) - 1
|
|
68
|
+
|
|
69
|
+
logger.debug('人氣', online_user)
|
|
70
|
+
|
|
71
|
+
target_list = [
|
|
72
|
+
# 新版「看板設定與管理」畫面沒有任意鍵提示;等頁尾畫完再解析
|
|
73
|
+
connect_core.TargetUnit('(PgUp)上頁 (PgDn)下頁', log_level=log.DEBUG if call_by_others else log.INFO,
|
|
74
|
+
break_detect=True),
|
|
75
|
+
connect_core.TargetUnit('任意鍵繼續', log_level=log.DEBUG if call_by_others else log.INFO,
|
|
76
|
+
break_detect=True),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
api.connect_core.send(
|
|
80
|
+
'i',
|
|
81
|
+
target_list)
|
|
82
|
+
|
|
83
|
+
# Same flag window as above: the 'i' send() right before is the last one
|
|
84
|
+
# until the get_post_kind block, so the flag still describes it here.
|
|
85
|
+
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
86
|
+
# print(ori_screen)
|
|
87
|
+
|
|
88
|
+
# 新版「看板設定與管理」沒有任意鍵提示,會一直停在選單上 (之後 API 開頭的 Space
|
|
89
|
+
# 會落在選單裡)。畫面已存下來,先用一個 ← 退回文章列表再解析;確認不了就斷線。
|
|
90
|
+
if screens.in_board_setting(ori_screen):
|
|
91
|
+
api.connect_core.send(command.left, [
|
|
92
|
+
connect_core.TargetUnit(screens.Target.InBoard, log_level=log.DEBUG, break_detect=True)])
|
|
93
|
+
if not _api_util.is_article_list(api, api.connect_core.get_screen_queue()[-1]):
|
|
94
|
+
_api_util.close_session(api)
|
|
95
|
+
raise exceptions.ConnectionClosed()
|
|
96
|
+
|
|
97
|
+
boardname = None
|
|
98
|
+
# 新版: '看板名稱: Test 板主名單: ...';舊版: '《Test》看板設定'
|
|
99
|
+
r = re.search(r'看板名稱: (\S+)|《(.+)》看板設定', ori_screen)
|
|
100
|
+
if r is not None:
|
|
101
|
+
boardname = (r.group(1) or r.group(2)).strip()
|
|
102
|
+
|
|
103
|
+
if boardname is None:
|
|
104
|
+
if api.connect_core.last_timeout_was_silent:
|
|
105
|
+
raise exceptions.ConnectionClosed()
|
|
106
|
+
raise exceptions.NoSuchBoard(api.config, board)
|
|
107
|
+
logger.debug('看板名稱', boardname, board)
|
|
108
|
+
|
|
109
|
+
if boardname.lower() != board.lower():
|
|
110
|
+
if api.connect_core.last_timeout_was_silent:
|
|
111
|
+
raise exceptions.ConnectionClosed()
|
|
112
|
+
raise exceptions.NoSuchBoard(api.config, board)
|
|
113
|
+
|
|
114
|
+
chinese_des = ''
|
|
115
|
+
p = re.compile(r'中文敘述: (.+)')
|
|
116
|
+
r = p.search(ori_screen)
|
|
117
|
+
if r is not None:
|
|
118
|
+
chinese_des = r.group(0)[5:].strip()
|
|
119
|
+
logger.debug('中文敘述', chinese_des)
|
|
120
|
+
|
|
121
|
+
moderators = []
|
|
122
|
+
p = re.compile(r'板主名單: (.+)')
|
|
123
|
+
r = p.search(ori_screen)
|
|
124
|
+
if r is not None:
|
|
125
|
+
moderator_line = r.group(0)[5:].strip()
|
|
126
|
+
if '(無)' in moderator_line:
|
|
127
|
+
moderators = []
|
|
128
|
+
else:
|
|
129
|
+
moderators = moderator_line.split('/')
|
|
130
|
+
for moderator in moderators.copy():
|
|
131
|
+
if moderator in ('徵求中', ''):
|
|
132
|
+
moderators.remove(moderator)
|
|
133
|
+
logger.debug('板主名單', moderators)
|
|
134
|
+
|
|
135
|
+
if '看板名稱:' in ori_screen:
|
|
136
|
+
# 新版畫面每項一列: ' 9. 快速連推文章 限制 (間隔 5 秒)'。
|
|
137
|
+
# 2026-10-19 起欄寬變寬;編輯模式另有熱鍵標示 ' 1. (b)中文敘述' 與
|
|
138
|
+
# 權限註記 '開放 (限站長)',兩者都去掉再比對。
|
|
139
|
+
# 值要整串比對:'開放' 是 '不開放' 的子字串,'對齊' 是 '不用對齊' 的子字串。
|
|
140
|
+
def value(label: str) -> str:
|
|
141
|
+
m = re.search(r'^[> ]*\d+\. +(?:\(\w\))?' + re.escape(label) + r' +(.+?) *$', ori_screen, re.M)
|
|
142
|
+
return re.sub(r' *\(限[^)]*\)$', '', m.group(1)) if m else ''
|
|
143
|
+
|
|
144
|
+
chinese_des = value('中文敘述')
|
|
145
|
+
open_status = value('公開狀態 (是否隱形)') == '公開'
|
|
146
|
+
into_top_ten_when_hide = value('隱板時進入十大排行榜') == '可以'
|
|
147
|
+
non_board_members_post = value('非看板會員發文') == '開放'
|
|
148
|
+
reply_post = value('回應文章') == '開放'
|
|
149
|
+
self_del_post = value('自刪文章') == '開放'
|
|
150
|
+
push_post = value('推薦文章 (推文)') == '開放'
|
|
151
|
+
boo_post = value('噓文') == '開放'
|
|
152
|
+
fast_push = value('快速連推文章') == '開放'
|
|
153
|
+
m = re.search(r'間隔 (\d+) 秒', value('快速連推文章'))
|
|
154
|
+
min_interval = int(m.group(1)) if m else 0
|
|
155
|
+
push_record_ip = value('推文時記錄來源 IP') == '自動記錄'
|
|
156
|
+
push_aligned = value('推文開頭自動對齊') == '對齊'
|
|
157
|
+
moderator_can_del_illegal_content = value('板主刪除部份違規文字') == '可'
|
|
158
|
+
tran_post_auto_recorded_and_require_post_permissions = value('轉錄文章自動記錄') == '會 (需發文權限)'
|
|
159
|
+
cool_mode = value('冷靜模式') != '未設定'
|
|
160
|
+
require18 = value('未滿十八歲進入') == '禁止'
|
|
161
|
+
# 2026-10-19 起有發文條件兩列:
|
|
162
|
+
# '16. [發文條件] 登入次數 500 次以上 (您: 6614 次)'
|
|
163
|
+
# '17. [發文條件] 退文篇數 1 篇以下 (您: 0 篇)'
|
|
164
|
+
# '無限制' 或沒有這兩列 (2026-10-18 的畫面) 都給 0 (= 無限制)
|
|
165
|
+
m = re.match(r'(\d+) 次以上', value('[發文條件] 登入次數'))
|
|
166
|
+
require_login_time = int(m.group(1)) if m else 0
|
|
167
|
+
m = re.match(r'(\d+) 篇以下', value('[發文條件] 退文篇數'))
|
|
168
|
+
require_illegal_post = int(m.group(1)) if m else 0
|
|
169
|
+
else:
|
|
170
|
+
open_status = ('公開狀態(是否隱形): 公開' in ori_screen)
|
|
171
|
+
logger.debug('公開狀態', open_status)
|
|
172
|
+
|
|
173
|
+
into_top_ten_when_hide = ('隱板時 可以 進入十大排行榜' in ori_screen)
|
|
174
|
+
logger.debug('隱板時可以進入十大排行榜', into_top_ten_when_hide)
|
|
175
|
+
|
|
176
|
+
non_board_members_post = ('開放 非看板會員發文' in ori_screen)
|
|
177
|
+
logger.debug('非看板會員發文', non_board_members_post)
|
|
178
|
+
|
|
179
|
+
reply_post = ('開放 回應文章' in ori_screen)
|
|
180
|
+
logger.debug('回應文章', reply_post)
|
|
181
|
+
|
|
182
|
+
self_del_post = ('開放 自刪文章' in ori_screen)
|
|
183
|
+
logger.debug('自刪文章', self_del_post)
|
|
184
|
+
|
|
185
|
+
push_post = ('開放 推薦文章' in ori_screen)
|
|
186
|
+
logger.debug('推薦文章', push_post)
|
|
187
|
+
|
|
188
|
+
boo_post = ('開放 噓文' in ori_screen)
|
|
189
|
+
logger.debug('噓文', boo_post)
|
|
190
|
+
|
|
191
|
+
# 限制 快速連推文章, 最低間隔時間: 5 秒
|
|
192
|
+
# 開放 快速連推文章
|
|
193
|
+
|
|
194
|
+
fast_push = ('開放 快速連推文章' in ori_screen)
|
|
195
|
+
logger.debug('快速連推文章', fast_push)
|
|
196
|
+
|
|
197
|
+
if not fast_push:
|
|
198
|
+
p = re.compile(r'最低間隔時間: [\d]+')
|
|
199
|
+
r = p.search(ori_screen)
|
|
200
|
+
if r is not None:
|
|
201
|
+
min_interval = r.group(0)[7:].strip()
|
|
202
|
+
min_interval = int(min_interval)
|
|
203
|
+
else:
|
|
204
|
+
min_interval = 0
|
|
205
|
+
logger.debug('最低間隔時間', min_interval)
|
|
206
|
+
else:
|
|
207
|
+
min_interval = 0
|
|
208
|
+
|
|
209
|
+
# 推文時 自動 記錄來源 IP
|
|
210
|
+
# 推文時 不會 記錄來源 IP
|
|
211
|
+
push_record_ip = ('推文時 自動 記錄來源 IP' in ori_screen)
|
|
212
|
+
logger.debug('記錄來源 IP', push_record_ip)
|
|
213
|
+
|
|
214
|
+
# 推文時 對齊 開頭
|
|
215
|
+
# 推文時 不用對齊 開頭
|
|
216
|
+
push_aligned = ('推文時 對齊 開頭' in ori_screen)
|
|
217
|
+
logger.debug('對齊開頭', push_aligned)
|
|
218
|
+
|
|
219
|
+
# 板主 可 刪除部份違規文字
|
|
220
|
+
moderator_can_del_illegal_content = ('板主 可 刪除部份違規文字' in ori_screen)
|
|
221
|
+
logger.debug('板主可刪除部份違規文字', moderator_can_del_illegal_content)
|
|
222
|
+
|
|
223
|
+
# 轉錄文章 會 自動記錄,且 需要 發文權限
|
|
224
|
+
tran_post_auto_recorded_and_require_post_permissions = ('轉錄文章 會 自動記錄,且 需要 發文權限' in ori_screen)
|
|
225
|
+
logger.debug('轉錄文章 會 自動記錄,且 需要 發文權限', tran_post_auto_recorded_and_require_post_permissions)
|
|
226
|
+
|
|
227
|
+
cool_mode = ('未 設為冷靜模式' not in ori_screen)
|
|
228
|
+
logger.debug('冷靜模式', cool_mode)
|
|
229
|
+
|
|
230
|
+
require18 = ('禁止 未滿十八歲進入' in ori_screen)
|
|
231
|
+
logger.debug('禁止未滿十八歲進入', require18)
|
|
232
|
+
|
|
233
|
+
p = re.compile(r'登入次數 [\d]+ 次以上')
|
|
234
|
+
r = p.search(ori_screen)
|
|
235
|
+
if r is not None:
|
|
236
|
+
require_login_time = r.group(0).split(' ')[1]
|
|
237
|
+
require_login_time = int(require_login_time)
|
|
238
|
+
else:
|
|
239
|
+
require_login_time = 0
|
|
240
|
+
logger.debug('發文限制登入次數', require_login_time)
|
|
241
|
+
|
|
242
|
+
p = re.compile(r'退文篇數 [\d]+ 篇以下')
|
|
243
|
+
r = p.search(ori_screen)
|
|
244
|
+
if r is not None:
|
|
245
|
+
require_illegal_post = r.group(0).split(' ')[1]
|
|
246
|
+
require_illegal_post = int(require_illegal_post)
|
|
247
|
+
else:
|
|
248
|
+
require_illegal_post = 0
|
|
249
|
+
logger.debug('發文限制退文篇數', require_illegal_post)
|
|
250
|
+
|
|
251
|
+
kind_list = []
|
|
252
|
+
if get_post_kind:
|
|
253
|
+
|
|
254
|
+
_api_util.goto_board(api, board)
|
|
255
|
+
|
|
256
|
+
# Go certain board, then post to get post type info
|
|
257
|
+
cmd_list = []
|
|
258
|
+
cmd_list.append(command.ctrl_p)
|
|
259
|
+
cmd = ''.join(cmd_list)
|
|
260
|
+
|
|
261
|
+
target_list = [
|
|
262
|
+
connect_core.TargetUnit('無法發文: 未達看板要求權限', break_detect=True),
|
|
263
|
+
connect_core.TargetUnit('或不選)', break_detect=True)
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
index = api.connect_core.send(
|
|
267
|
+
cmd,
|
|
268
|
+
target_list)
|
|
269
|
+
|
|
270
|
+
if index == 0:
|
|
271
|
+
raise exceptions.NoPermission(i18n.no_permission)
|
|
272
|
+
# no post permission
|
|
273
|
+
|
|
274
|
+
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
275
|
+
screen_lines = ori_screen.split('\n')
|
|
276
|
+
|
|
277
|
+
for i in screen_lines:
|
|
278
|
+
if '種類:' in i:
|
|
279
|
+
type_pattern = re.compile(r'\d\.([^\ ]*)')
|
|
280
|
+
# 0 is not present any type that the key hold None object
|
|
281
|
+
kind_list = type_pattern.findall(i)
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
# Clear post status
|
|
285
|
+
cmd_list = []
|
|
286
|
+
cmd_list.append(command.ctrl_c)
|
|
287
|
+
cmd_list.append(command.ctrl_c)
|
|
288
|
+
cmd = ''.join(cmd_list)
|
|
289
|
+
|
|
290
|
+
target_list = [
|
|
291
|
+
connect_core.TargetUnit(screens.Target.InBoard, break_detect=True)
|
|
292
|
+
]
|
|
293
|
+
api.connect_core.send(
|
|
294
|
+
cmd,
|
|
295
|
+
target_list)
|
|
296
|
+
|
|
297
|
+
logger.info(
|
|
298
|
+
i18n.replace(i18n.get_board_info, board),
|
|
299
|
+
'...', i18n.success
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
BoardField.board: boardname,
|
|
304
|
+
BoardField.online_user: online_user,
|
|
305
|
+
BoardField.mandarin_des: chinese_des,
|
|
306
|
+
BoardField.moderators: moderators,
|
|
307
|
+
BoardField.open_status: open_status,
|
|
308
|
+
BoardField.into_top_ten_when_hide: into_top_ten_when_hide,
|
|
309
|
+
BoardField.can_non_board_members_post: non_board_members_post,
|
|
310
|
+
BoardField.can_reply_post: reply_post,
|
|
311
|
+
BoardField.self_del_post: self_del_post,
|
|
312
|
+
BoardField.can_comment_post: push_post,
|
|
313
|
+
BoardField.can_boo_post: boo_post,
|
|
314
|
+
BoardField.can_fast_push: fast_push,
|
|
315
|
+
BoardField.min_interval_between_comments: min_interval,
|
|
316
|
+
BoardField.is_comment_record_ip: push_record_ip,
|
|
317
|
+
BoardField.is_comment_aligned: push_aligned,
|
|
318
|
+
BoardField.can_moderators_del_illegal_content: moderator_can_del_illegal_content,
|
|
319
|
+
BoardField.does_tran_post_auto_recorded_and_require_post_permissions: tran_post_auto_recorded_and_require_post_permissions,
|
|
320
|
+
BoardField.is_cool_mode: cool_mode,
|
|
321
|
+
BoardField.is_require18: require18,
|
|
322
|
+
BoardField.require_login_time: require_login_time,
|
|
323
|
+
BoardField.require_illegal_post: require_illegal_post,
|
|
324
|
+
BoardField.post_kind_list: kind_list
|
|
325
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
1
3
|
from . import _api_util
|
|
2
4
|
from . import command
|
|
3
5
|
from . import connect_core
|
|
4
6
|
from . import exceptions
|
|
5
7
|
from . import i18n
|
|
6
8
|
from . import log
|
|
9
|
+
from . import screens
|
|
7
10
|
from .data_type import FavouriteBoardField
|
|
8
11
|
|
|
12
|
+
# ' 83) MCP_Test 測試 ◎…'、' 2 ˇAllTogether 聯誼 ◎…':編號、[)]、[ˇ]、板名、類別、◎標題
|
|
13
|
+
_ROW = re.compile(r'\s*\d+\)?\s+ˇ?\s*(\S+)\s+(\S+)\s*(.*)')
|
|
14
|
+
|
|
9
15
|
|
|
10
16
|
def get_favourite_board(api) -> list:
|
|
11
17
|
_api_util.one_thread(api)
|
|
@@ -17,7 +23,8 @@ def get_favourite_board(api) -> list:
|
|
|
17
23
|
cmd = ''.join(cmd_list)
|
|
18
24
|
|
|
19
25
|
target_list = [
|
|
20
|
-
|
|
26
|
+
# 舊版頁尾有「選擇看板」,新版沒有;標頭與頁尾的「已讀/未讀」新舊都有
|
|
27
|
+
connect_core.TargetUnit(['【看板列表】', '已讀/未讀'], break_detect=True)
|
|
21
28
|
]
|
|
22
29
|
|
|
23
30
|
log.logger.info(i18n.get_favourite_board_list)
|
|
@@ -32,42 +39,20 @@ def get_favourite_board(api) -> list:
|
|
|
32
39
|
target_list)
|
|
33
40
|
|
|
34
41
|
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
35
|
-
|
|
36
|
-
screen_buf = ori_screen
|
|
37
|
-
screen_buf = [x for x in screen_buf.split('\n')][3:-1]
|
|
38
|
-
|
|
39
|
-
# adjust for cursor
|
|
40
|
-
screen_buf[0] = ' ' + screen_buf[0][1:]
|
|
41
|
-
screen_buf = [x for x in screen_buf]
|
|
42
|
-
|
|
43
|
-
min_len = 47
|
|
44
|
-
|
|
45
|
-
for i, line in enumerate(screen_buf):
|
|
46
|
-
if len(screen_buf[i]) == 0:
|
|
47
|
-
continue
|
|
48
|
-
if len(screen_buf[i]) <= min_len:
|
|
49
|
-
# print(f'[{ScreenBuf[i]}]')
|
|
50
|
-
screen_buf[i] = screen_buf[i] + (' ' * ((min_len + 1) - len(screen_buf[i])))
|
|
51
|
-
screen_buf = [x[10:min_len - len(x)].strip() for x in screen_buf]
|
|
52
|
-
screen_buf = list(filter(None, screen_buf))
|
|
42
|
+
screen_buf = [x for x in ori_screen.split('\n')[3:-1] if x.strip()]
|
|
53
43
|
|
|
54
|
-
for
|
|
44
|
+
for line in screen_buf:
|
|
55
45
|
if '------------' in line:
|
|
56
46
|
continue
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
board_type = no_space_temp[1]
|
|
66
|
-
|
|
67
|
-
title_start_index = temp.index(board_type) + 1
|
|
68
|
-
board_title = ' '.join(temp[title_start_index:])
|
|
47
|
+
if line.startswith(api.cursor):
|
|
48
|
+
line = ' ' * screens._cell_len(api.cursor) + line[len(api.cursor):]
|
|
49
|
+
# 板名、類別依欄位內容切;標題與人氣欄之間沒有分隔,只能在第 64 格 (人氣欄起點) 切掉
|
|
50
|
+
m = _ROW.match(line[:screens._str_pos_at_cells(line, 64)])
|
|
51
|
+
if m is None:
|
|
52
|
+
continue
|
|
53
|
+
board, board_type = m.group(1), m.group(2)
|
|
69
54
|
# remove ◎
|
|
70
|
-
board_title =
|
|
55
|
+
board_title = m.group(3)[1:].strip()
|
|
71
56
|
|
|
72
57
|
if board in board_list:
|
|
73
58
|
log.logger.info(i18n.get_favourite_board_list, '...', i18n.success)
|
|
@@ -119,6 +119,12 @@ def get_newest_index(api, index_type: data_type.NewIndex, board: Optional[str] =
|
|
|
119
119
|
screens.Target.InBoardWithCursor,
|
|
120
120
|
log_level=log.DEBUG,
|
|
121
121
|
break_detect=True))
|
|
122
|
+
# 新版搜尋無結果時,後面的 '1\r' 會打開游標所在的文章;落在文章裡就是沒有結果
|
|
123
|
+
# 游標文是動畫文時會先停在「要開始播放嗎?」;同樣是沒有結果 (畫面留在詢問上,由下一支 API 的導航離開)
|
|
124
|
+
target_list.append(connect_core.TargetUnit(screens.Target.InPost,
|
|
125
|
+
exceptions_=exceptions.NoSearchResult()))
|
|
126
|
+
target_list.append(connect_core.TargetUnit(screens.Target.Animation,
|
|
127
|
+
exceptions_=exceptions.NoSearchResult()))
|
|
122
128
|
|
|
123
129
|
cmd_list = _api_util.get_search_condition_cmd(index_type, search_list)
|
|
124
130
|
|
|
@@ -54,6 +54,9 @@ def get_post_list(api, board: str, limit: int = 20, offset: int = 0) -> List[Dic
|
|
|
54
54
|
|
|
55
55
|
post_list = []
|
|
56
56
|
|
|
57
|
+
# get_newest_index 命中 2 秒快取時不會導航,畫面可能還停在上一支 API 留下的地方 (例如 get_post 的文章內)
|
|
58
|
+
_api_util.goto_board(api, board)
|
|
59
|
+
|
|
57
60
|
with _api_util.expanded_screen(api):
|
|
58
61
|
for _ in range(max_round):
|
|
59
62
|
|
|
@@ -71,7 +71,8 @@ def give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag_content
|
|
|
71
71
|
|
|
72
72
|
target_list = [
|
|
73
73
|
connect_core.TargetUnit('你沒有那麼多Ptt幣喔!', break_detect=True, exceptions_=exceptions.NoMoney()),
|
|
74
|
-
connect_core.TargetUnit('金額過少,交易取消!', break_detect=True,
|
|
74
|
+
connect_core.TargetUnit('金額過少,交易取消!', break_detect=True,
|
|
75
|
+
exceptions_=exceptions.ParameterError(f'money is too small: {money}')),
|
|
75
76
|
connect_core.TargetUnit('交易取消!', break_detect=True,
|
|
76
77
|
exceptions_=exceptions.UnknownError(i18n.transaction_cancelled)),
|
|
77
78
|
connect_core.TargetUnit('確定進行交易嗎?', response='y' + command.enter),
|
|
@@ -80,10 +81,13 @@ def give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag_content
|
|
|
80
81
|
connect_core.TargetUnit('要修改紅包袋嗎', response=command.enter),
|
|
81
82
|
connect_core.TargetUnit('完成交易前要重新確認您的身份', response=api._ptt_pw + command.enter, secret=True),
|
|
82
83
|
connect_core.TargetUnit('他是你的小主人,是否匿名?', response='n' + command.enter),
|
|
83
|
-
|
|
84
|
-
connect_core.TargetUnit('這位幸運兒的id', response=ptt_id + command.enter),
|
|
84
|
+
# 金額那行在後續畫面都還在,所以後續的確認提示要排在「幣呢?」前面,否則金額會被打在 y/n 提示上
|
|
85
85
|
connect_core.TargetUnit('認證尚未過期', response='y' + command.enter),
|
|
86
|
-
connect_core.TargetUnit('交易正在進行中', response=command.space)
|
|
86
|
+
connect_core.TargetUnit('交易正在進行中', response=command.space),
|
|
87
|
+
# 舊版「要給他多少Ptt幣呢?」、新版「要給多少Ptt幣呢?」(docker 站是 Dtt幣)。要排在「這位幸運兒的id」
|
|
88
|
+
# 前面,否則同畫面的 id 行先被對上,id 又被送進金額欄。TAB 切到「對方收到 (稅後)」,與舊版相同
|
|
89
|
+
connect_core.TargetUnit('幣呢?', response=command.tab + str(money) + command.enter),
|
|
90
|
+
connect_core.TargetUnit('這位幸運兒的id', response=ptt_id + command.enter),
|
|
87
91
|
]
|
|
88
92
|
|
|
89
93
|
api.connect_core.send(
|