PyPtt 2.0.7__tar.gz → 2.0.9__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.0.7 → pyptt-2.0.9}/PKG-INFO +1 -1
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/PTT.py +6 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/__init__.py +1 -1
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_bucket.py +1 -1
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_call_status.py +11 -8
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_comment.py +1 -1
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_board_info.py +5 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_user.py +18 -6
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_give_money.py +1 -2
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_mail.py +2 -2
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_mark_post.py +1 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_reply_post.py +2 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_util.py +7 -2
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/config.py +3 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/connect_core.py +36 -20
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/data_type.py +8 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/screens.py +3 -3
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/service.py +32 -3
- pyptt-2.0.9/PyPtt/ssl_config.py +24 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt.egg-info/PKG-INFO +1 -1
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_user.py +17 -2
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_service.py +47 -0
- pyptt-2.0.7/PyPtt/ssl_config.py +0 -24
- {pyptt-2.0.7 → pyptt-2.0.9}/LICENSE +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_change_pw.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_del_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_board_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_bottom_post_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_favourite_board.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_newest_index.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_post_index.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_post_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_time.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_get_waterball.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_has_new_mail.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_loginout.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_search_user.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/_api_set_board_title.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/api_server.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/check_value.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/command.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/exceptions.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/i18n.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/lang_en_US.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/lang_zh_TW.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/lib_util.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt/log.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt.egg-info/SOURCES.txt +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt.egg-info/dependency_links.txt +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt.egg-info/requires.txt +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/PyPtt.egg-info/top_level.txt +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/README.md +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/pyproject.toml +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/setup.cfg +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/setup.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_change_pw.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_comment.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_del_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_exceptions.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_board_info.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_board_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_bottom_post_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_favourite_boards.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_mail.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_newest_index.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_post_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_post_parser.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_time.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_get_waterball.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_give_money.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_i18n.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_init.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_logger.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_parse_query_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_reply_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.9}/tests/test_search_user.py +0 -0
|
@@ -53,6 +53,7 @@ class API:
|
|
|
53
53
|
logger_callback (Callable): PyPtt 顯示訊息的 callback。預設為 None。
|
|
54
54
|
port (int): PyPtt 連線的 port。預設為 **23**。
|
|
55
55
|
host (:ref:`host`): PyPtt 連線的 PTT 伺服器。預設為 **PTT1**。
|
|
56
|
+
verify_ssl (bool): 是否驗證 PTT server 端的 TLS 憑證。預設為 **True**。若你的網路環境透過 SSL 攔截 proxy(如企業防火牆),可設為 **False** 以停用驗證。
|
|
56
57
|
check_update (bool): 是否檢查 PyPtt 的更新。預設為 **False**。
|
|
57
58
|
|
|
58
59
|
Returns:
|
|
@@ -145,8 +146,13 @@ class API:
|
|
|
145
146
|
raise exceptions.ParameterError('[PyPtt] TELNET is not available on PTT1 and PTT2')
|
|
146
147
|
self.config.connect_mode = connect_mode
|
|
147
148
|
|
|
149
|
+
verify_ssl = kwargs.get('verify_ssl', True)
|
|
150
|
+
check_value.check_type(verify_ssl, bool, 'verify_ssl')
|
|
151
|
+
self.config.verify_ssl = verify_ssl
|
|
152
|
+
|
|
148
153
|
self.connect_core = connect_core.API(self)
|
|
149
154
|
self._exist_board_list = set()
|
|
155
|
+
self._exist_user_list = set()
|
|
150
156
|
self._moderators = dict()
|
|
151
157
|
self._thread_id = threading.get_ident()
|
|
152
158
|
self._goto_board_list = set()
|
|
@@ -22,7 +22,7 @@ def bucket(api, board: str, bucket_days: int, reason: str, ptt_id: str) -> None:
|
|
|
22
22
|
check_value.check_type(reason, str, 'reason')
|
|
23
23
|
check_value.check_type(ptt_id, str, 'ptt_id')
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
_api_util.check_user_exist(api, ptt_id)
|
|
26
26
|
|
|
27
27
|
_api_util.check_board(api, board, check_moderator=True)
|
|
28
28
|
|
|
@@ -6,8 +6,7 @@ from . import log
|
|
|
6
6
|
from . import screens
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def get_call_status(api) ->
|
|
10
|
-
# log.py = DefaultLogger('api', api.config.log_level)
|
|
9
|
+
def get_call_status(api) -> data_type.CallStatus:
|
|
11
10
|
|
|
12
11
|
cmd_list = []
|
|
13
12
|
cmd_list.append(command.go_main_menu)
|
|
@@ -34,15 +33,15 @@ def get_call_status(api) -> None:
|
|
|
34
33
|
raise exceptions.UnknownError('UnknownError')
|
|
35
34
|
|
|
36
35
|
if index == 0:
|
|
37
|
-
return data_type.
|
|
36
|
+
return data_type.CallStatus.ON
|
|
38
37
|
if index == 1:
|
|
39
|
-
return data_type.
|
|
38
|
+
return data_type.CallStatus.UNPLUG
|
|
40
39
|
if index == 2:
|
|
41
|
-
return data_type.
|
|
40
|
+
return data_type.CallStatus.WATERPROOF
|
|
42
41
|
if index == 3:
|
|
43
|
-
return data_type.
|
|
42
|
+
return data_type.CallStatus.FRIEND
|
|
44
43
|
if index == 4:
|
|
45
|
-
return data_type.
|
|
44
|
+
return data_type.CallStatus.OFF
|
|
46
45
|
|
|
47
46
|
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
48
47
|
raise exceptions.UnknownError(ori_screen)
|
|
@@ -63,10 +62,14 @@ def set_call_status(api, call_status) -> None:
|
|
|
63
62
|
target_list = [
|
|
64
63
|
connect_core.TargetUnit(screens.Target.InUserList, break_detect=True)]
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
for _ in range(6):
|
|
66
|
+
if current_call_status == call_status:
|
|
67
|
+
break
|
|
67
68
|
api.connect_core.send(
|
|
68
69
|
cmd,
|
|
69
70
|
target_list,
|
|
70
71
|
screen_timeout=api.config.screen_long_timeout)
|
|
71
72
|
|
|
72
73
|
current_call_status = api._get_call_status()
|
|
74
|
+
else:
|
|
75
|
+
raise exceptions.UnknownError('could not set call status')
|
|
@@ -69,22 +69,27 @@ def get_board_info(api, board: str, get_post_kind: bool, call_by_others: bool) -
|
|
|
69
69
|
ori_screen = api.connect_core.get_screen_queue()[-1]
|
|
70
70
|
# print(ori_screen)
|
|
71
71
|
|
|
72
|
+
boardname = None
|
|
72
73
|
p = re.compile(r'《(.+)》看板設定')
|
|
73
74
|
r = p.search(ori_screen)
|
|
74
75
|
if r is not None:
|
|
75
76
|
boardname = r.group(0)[1:-5].strip()
|
|
76
77
|
|
|
78
|
+
if boardname is None:
|
|
79
|
+
raise exceptions.NoSuchBoard(api.config, board)
|
|
77
80
|
logger.debug('看板名稱', boardname, board)
|
|
78
81
|
|
|
79
82
|
if boardname.lower() != board.lower():
|
|
80
83
|
raise exceptions.NoSuchBoard(api.config, board)
|
|
81
84
|
|
|
85
|
+
chinese_des = ''
|
|
82
86
|
p = re.compile(r'中文敘述: (.+)')
|
|
83
87
|
r = p.search(ori_screen)
|
|
84
88
|
if r is not None:
|
|
85
89
|
chinese_des = r.group(0)[5:].strip()
|
|
86
90
|
logger.debug('中文敘述', chinese_des)
|
|
87
91
|
|
|
92
|
+
moderators = []
|
|
88
93
|
p = re.compile(r'板主名單: (.+)')
|
|
89
94
|
r = p.search(ori_screen)
|
|
90
95
|
if r is not None:
|
|
@@ -30,12 +30,21 @@ def get_user(api, ptt_id: str) -> Dict:
|
|
|
30
30
|
if len(ptt_id) < 2:
|
|
31
31
|
raise exceptions.ParameterError(f'wrong parameter user_id: {ptt_id}')
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cmd_list.append(command.enter)
|
|
33
|
+
# Step 1: Navigate to Talk menu.
|
|
34
|
+
# AnyKey auto-dismiss is safe here because no query commands are pending.
|
|
35
|
+
cmd = command.go_main_menu + 'T' + command.enter
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
target_list = [
|
|
38
|
+
connect_core.TargetUnit(screens.Target.InTalk, break_detect=True),
|
|
39
|
+
connect_core.TargetUnit(screens.Target.AnyKey, response=' '),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
nav_index = api.connect_core.send(cmd, target_list)
|
|
43
|
+
|
|
44
|
+
# Step 2: Query user.
|
|
45
|
+
# Use AnyKey as break target — it's at the very bottom of the user info
|
|
46
|
+
# screen, so matching it confirms the screen is fully transmitted.
|
|
47
|
+
cmd = 'Q' + command.enter + ptt_id + command.enter
|
|
39
48
|
|
|
40
49
|
# Use InTalk as a non-break target that auto-responds with the
|
|
41
50
|
# query command, and max_match=1 to prevent re-matching on
|
|
@@ -113,7 +122,8 @@ def get_user(api, ptt_id: str) -> Dict:
|
|
|
113
122
|
|
|
114
123
|
# PTT2 沒有退文
|
|
115
124
|
if api.config.host == data_type.HOST.PTT1:
|
|
116
|
-
|
|
125
|
+
int_list = re.findall(r'\d+', buff_1)
|
|
126
|
+
illegal_post = int(int_list[1]) if len(int_list) > 1 else 0
|
|
117
127
|
else:
|
|
118
128
|
illegal_post = None
|
|
119
129
|
|
|
@@ -152,5 +162,7 @@ def get_user(api, ptt_id: str) -> Dict:
|
|
|
152
162
|
UserField.chess: chess,
|
|
153
163
|
UserField.signature_file: signature_file,
|
|
154
164
|
}
|
|
165
|
+
api._exist_user_list.add(ptt_id.lower())
|
|
166
|
+
|
|
155
167
|
user = json.dumps(user, cls=AutoJsonEncoder)
|
|
156
168
|
return json.loads(user)
|
|
@@ -33,8 +33,7 @@ def give_money(api, ptt_id: str, money: int, red_bag_title: str, red_bag_content
|
|
|
33
33
|
log.logger.info(
|
|
34
34
|
i18n.replace(i18n.give_money_to, ptt_id, money))
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
api.get_user(ptt_id)
|
|
36
|
+
_api_util.check_user_exist(api, ptt_id)
|
|
38
37
|
|
|
39
38
|
cmd_list = []
|
|
40
39
|
cmd_list.append(command.go_main_menu)
|
|
@@ -35,7 +35,7 @@ def mail(api,
|
|
|
35
35
|
check_value.check_type(title, str, 'title')
|
|
36
36
|
check_value.check_type(content, str, 'content')
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
_api_util.check_user_exist(api, ptt_id)
|
|
39
39
|
|
|
40
40
|
check_sign_file = False
|
|
41
41
|
for i in range(0, 10):
|
|
@@ -277,7 +277,7 @@ def del_mail(api, index) -> None:
|
|
|
277
277
|
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
|
|
278
278
|
|
|
279
279
|
current_index = api.get_newest_index(data_type.NewIndex.MAIL)
|
|
280
|
-
check_value.check_index(index, current_index)
|
|
280
|
+
check_value.check_index('index', index, current_index)
|
|
281
281
|
|
|
282
282
|
cmd_list = []
|
|
283
283
|
# 進入主選單
|
|
@@ -68,6 +68,8 @@ def reply_post(api, reply_to: data_type.ReplyTo, board: str, content: str, sign_
|
|
|
68
68
|
elif reply_to == data_type.ReplyTo.BOARD_MAIL:
|
|
69
69
|
reply_msg = i18n.reply_board_mail
|
|
70
70
|
reply_target_unit = connect_core.TargetUnit('▲ 回應至', log_level=log.INFO, response='B' + command.enter)
|
|
71
|
+
else:
|
|
72
|
+
raise exceptions.ParameterError(f'unsupported reply_to: {reply_to}')
|
|
71
73
|
log.logger.info(reply_msg)
|
|
72
74
|
|
|
73
75
|
cmd = ''.join(cmd_list)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import functools
|
|
4
3
|
import re
|
|
5
4
|
import threading
|
|
6
5
|
from dataclasses import dataclass
|
|
@@ -62,6 +61,8 @@ def get_content(api, post_mode: bool = True):
|
|
|
62
61
|
first_page = True
|
|
63
62
|
origin_post = []
|
|
64
63
|
stop_dict = dict()
|
|
64
|
+
new_content_part = ''
|
|
65
|
+
get_line_b = 0
|
|
65
66
|
|
|
66
67
|
while True:
|
|
67
68
|
index = api.connect_core.send(cmd, target_list)
|
|
@@ -445,7 +446,11 @@ def one_thread(api):
|
|
|
445
446
|
raise exceptions.MultiThreadOperated()
|
|
446
447
|
|
|
447
448
|
|
|
448
|
-
|
|
449
|
+
def check_user_exist(api, ptt_id: str) -> None:
|
|
450
|
+
if ptt_id.lower() not in api._exist_user_list:
|
|
451
|
+
api.get_user(ptt_id)
|
|
452
|
+
|
|
453
|
+
|
|
449
454
|
def check_board(api, board: str, check_moderator: bool = False) -> Dict:
|
|
450
455
|
if board.lower() not in api._exist_board_list:
|
|
451
456
|
board_info = _api_get_board_info.get_board_info(api, board, get_post_kind=False, call_by_others=False)
|
|
@@ -32,12 +32,15 @@ except AttributeError:
|
|
|
32
32
|
websockets.http11.USER_AGENT += f' PyPtt/{PyPtt.__version__}'
|
|
33
33
|
use_http11 = True
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
def ssl_init(verify_ssl: bool = True) -> ssl.SSLContext:
|
|
36
|
+
# Verify the PTT server certificate against the system CA bundle.
|
|
37
|
+
# The client cert loaded below is *not* for identity (PTT does not
|
|
38
|
+
# validate it); it exists purely to make the TLS handshake succeed in
|
|
39
|
+
# environments where omitting a Certificate message breaks negotiation.
|
|
40
|
+
# See PyPtt/ssl_config.py for details.
|
|
41
|
+
#
|
|
42
|
+
# Pass verify_ssl=False only when connecting through an SSL-inspecting
|
|
43
|
+
# proxy whose CA cert is not in the system trust store.
|
|
41
44
|
cert_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pem')
|
|
42
45
|
cert_file.write(ssl_config.cert.encode('utf-8'))
|
|
43
46
|
|
|
@@ -47,17 +50,18 @@ def ssl_init():
|
|
|
47
50
|
cert_file.close()
|
|
48
51
|
key_file.close()
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
ctx = ssl.create_default_context()
|
|
54
|
+
ctx.load_cert_chain(certfile=cert_file.name, keyfile=key_file.name)
|
|
55
|
+
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
56
|
+
|
|
57
|
+
if not verify_ssl:
|
|
58
|
+
ctx.check_hostname = False
|
|
59
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
55
60
|
|
|
56
61
|
os.unlink(cert_file.name)
|
|
57
62
|
os.unlink(key_file.name)
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
ssl_init()
|
|
64
|
+
return ctx
|
|
61
65
|
|
|
62
66
|
|
|
63
67
|
class TargetUnit:
|
|
@@ -153,6 +157,7 @@ class API(object):
|
|
|
153
157
|
self._UseTooManyResources = TargetUnit(screens.Target.use_too_many_resources,
|
|
154
158
|
exceptions_=exceptions.UseTooManyResources())
|
|
155
159
|
self._loop = None
|
|
160
|
+
self._ssl_context = ssl_init(self.config.verify_ssl)
|
|
156
161
|
|
|
157
162
|
def _get_event_loop(self):
|
|
158
163
|
if self._loop and not self._loop.is_closed():
|
|
@@ -208,7 +213,7 @@ class API(object):
|
|
|
208
213
|
websockets.connect(
|
|
209
214
|
websocket_host,
|
|
210
215
|
origin=websocket_origin,
|
|
211
|
-
ssl=
|
|
216
|
+
ssl=self._ssl_context))
|
|
212
217
|
connect_success = True
|
|
213
218
|
break
|
|
214
219
|
except Exception as e:
|
|
@@ -249,6 +254,8 @@ class API(object):
|
|
|
249
254
|
self._RDQ.add(screen)
|
|
250
255
|
if target == self._UseTooManyResources:
|
|
251
256
|
use_too_many_res = True
|
|
257
|
+
find_target = True
|
|
258
|
+
log.logger.info(i18n.use_too_many_resources)
|
|
252
259
|
break
|
|
253
260
|
target.raise_exception()
|
|
254
261
|
|
|
@@ -327,19 +334,22 @@ class API(object):
|
|
|
327
334
|
raise exceptions.UseTooManyResources()
|
|
328
335
|
raise exceptions.ConnectionClosed()
|
|
329
336
|
|
|
337
|
+
if isinstance(data_chunk, str):
|
|
338
|
+
data_chunk = data_chunk.encode('utf-8')
|
|
330
339
|
receive_data_buffer += data_chunk
|
|
331
340
|
|
|
332
341
|
screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
|
|
333
342
|
self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)
|
|
334
343
|
|
|
335
|
-
if
|
|
336
|
-
self.current_encoding
|
|
344
|
+
if not find_target:
|
|
345
|
+
original_encoding = self.current_encoding
|
|
346
|
+
self.current_encoding = 'big5uao' if original_encoding == 'utf-8' else 'utf-8'
|
|
337
347
|
screen_, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
|
|
338
348
|
self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)
|
|
339
349
|
if find_target:
|
|
340
350
|
screen = screen_
|
|
341
351
|
else:
|
|
342
|
-
self.current_encoding =
|
|
352
|
+
self.current_encoding = original_encoding
|
|
343
353
|
|
|
344
354
|
if find_target:
|
|
345
355
|
break
|
|
@@ -348,13 +358,15 @@ class API(object):
|
|
|
348
358
|
vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding)
|
|
349
359
|
screens.show(self.config, vt100_p.screen)
|
|
350
360
|
self._RDQ.add(vt100_p.screen)
|
|
361
|
+
if use_too_many_res:
|
|
362
|
+
raise exceptions.UseTooManyResources()
|
|
351
363
|
return -1
|
|
352
364
|
|
|
353
365
|
if target_index != -1:
|
|
354
366
|
return target_index
|
|
355
367
|
|
|
356
368
|
if use_too_many_res:
|
|
357
|
-
|
|
369
|
+
raise exceptions.UseTooManyResources()
|
|
358
370
|
|
|
359
371
|
if not find_target:
|
|
360
372
|
return -1
|
|
@@ -366,7 +378,7 @@ class API(object):
|
|
|
366
378
|
raise exceptions.ParameterError('Item of TargetList must be TargetUnit')
|
|
367
379
|
|
|
368
380
|
if self._UseTooManyResources not in target_list:
|
|
369
|
-
target_list
|
|
381
|
+
target_list = target_list + [self._UseTooManyResources]
|
|
370
382
|
|
|
371
383
|
if self.config.connect_mode == data_type.ConnectMode.TELNET:
|
|
372
384
|
# Original Telnet logic remains, as it doesn't use asyncio
|
|
@@ -428,7 +440,11 @@ class API(object):
|
|
|
428
440
|
|
|
429
441
|
def close(self):
|
|
430
442
|
if self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:
|
|
431
|
-
|
|
443
|
+
try:
|
|
444
|
+
is_open = not self._core.closed
|
|
445
|
+
except AttributeError:
|
|
446
|
+
is_open = getattr(self._core, 'open', False)
|
|
447
|
+
if self._core and is_open:
|
|
432
448
|
loop = self._get_event_loop()
|
|
433
449
|
try:
|
|
434
450
|
loop.run_until_complete(asyncio.wait_for(self._core.close(), timeout=2.0))
|
|
@@ -98,6 +98,14 @@ class PostField(AutoStrEnum):
|
|
|
98
98
|
is_unconfirmed = auto()
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
class CallStatus(AutoStrEnum):
|
|
102
|
+
ON = auto()
|
|
103
|
+
UNPLUG = auto()
|
|
104
|
+
WATERPROOF = auto()
|
|
105
|
+
FRIEND = auto()
|
|
106
|
+
OFF = auto()
|
|
107
|
+
|
|
108
|
+
|
|
101
109
|
class WaterballType(AutoStrEnum):
|
|
102
110
|
SEND = auto()
|
|
103
111
|
CATCH = auto()
|
|
@@ -151,7 +151,7 @@ class Target:
|
|
|
151
151
|
]
|
|
152
152
|
|
|
153
153
|
use_too_many_resources = [
|
|
154
|
-
'
|
|
154
|
+
'程式耗用過多'
|
|
155
155
|
]
|
|
156
156
|
|
|
157
157
|
Animation = [
|
|
@@ -217,11 +217,11 @@ class VT100Parser:
|
|
|
217
217
|
|
|
218
218
|
def _move(self, x, y):
|
|
219
219
|
self._cursor_x = x
|
|
220
|
-
self._cursor_y = y
|
|
220
|
+
self._cursor_y = min(y, len(self.screen) - 1)
|
|
221
221
|
|
|
222
222
|
def _newline(self):
|
|
223
223
|
self._cursor_x = 0
|
|
224
|
-
self._cursor_y
|
|
224
|
+
self._cursor_y = min(self._cursor_y + 1, len(self.screen) - 1)
|
|
225
225
|
|
|
226
226
|
def _k(self):
|
|
227
227
|
if self._cursor_x == 0:
|
|
@@ -13,7 +13,7 @@ from . import log
|
|
|
13
13
|
|
|
14
14
|
class Service:
|
|
15
15
|
|
|
16
|
-
def __init__(self, pyptt_init_config: Optional[dict] = None):
|
|
16
|
+
def __init__(self, pyptt_init_config: Optional[dict] = None, call_interval: float = 0):
|
|
17
17
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
@@ -24,6 +24,7 @@ class Service:
|
|
|
24
24
|
|
|
25
25
|
Args:
|
|
26
26
|
pyptt_init_config (dict): PyPtt 初始化設定,請參考 :ref:`初始化設定 <api-init>`。
|
|
27
|
+
call_interval (float): API 呼叫之間的最小時間間隔(秒),預設為 0(不限制)。
|
|
27
28
|
|
|
28
29
|
Returns:
|
|
29
30
|
None
|
|
@@ -48,7 +49,7 @@ class Service:
|
|
|
48
49
|
# 'language': PyPtt.Language.ENGLISH,
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
service = Service(pyptt_init_config)
|
|
52
|
+
service = Service(pyptt_init_config, call_interval=0.5)
|
|
52
53
|
|
|
53
54
|
try:
|
|
54
55
|
service.call('login', {'ptt_id': 'YOUR_PTT_ID', 'ptt_pw': 'YOUR_PTT_PW'})
|
|
@@ -69,6 +70,10 @@ class Service:
|
|
|
69
70
|
if pyptt_init_config is None:
|
|
70
71
|
pyptt_init_config = {}
|
|
71
72
|
|
|
73
|
+
check_value.check_type(call_interval, (int, float), 'call_interval')
|
|
74
|
+
if call_interval < 0:
|
|
75
|
+
raise exceptions.ParameterError('call_interval must be >= 0')
|
|
76
|
+
|
|
72
77
|
log_level = pyptt_init_config.get('log_level', log.INFO)
|
|
73
78
|
self.logger = log.init(log_level, 'service')
|
|
74
79
|
|
|
@@ -77,6 +82,9 @@ class Service:
|
|
|
77
82
|
self._api = None
|
|
78
83
|
self._api_init_config = pyptt_init_config
|
|
79
84
|
|
|
85
|
+
self._call_interval = call_interval
|
|
86
|
+
self._last_call_time = 0.0
|
|
87
|
+
|
|
80
88
|
self._call_queue = Queue()
|
|
81
89
|
self._call_result = {}
|
|
82
90
|
self._call_result_cv = Condition()
|
|
@@ -106,6 +114,11 @@ class Service:
|
|
|
106
114
|
except queue.Empty:
|
|
107
115
|
continue
|
|
108
116
|
|
|
117
|
+
if self._call_interval > 0:
|
|
118
|
+
elapsed = time.monotonic() - self._last_call_time
|
|
119
|
+
if elapsed < self._call_interval:
|
|
120
|
+
time.sleep(self._call_interval - elapsed)
|
|
121
|
+
|
|
109
122
|
func = getattr(self._api, call['api'])
|
|
110
123
|
|
|
111
124
|
api_result = None
|
|
@@ -115,12 +128,14 @@ class Service:
|
|
|
115
128
|
except Exception as e:
|
|
116
129
|
api_exception = e
|
|
117
130
|
|
|
131
|
+
self._last_call_time = time.monotonic()
|
|
132
|
+
|
|
118
133
|
with self._call_result_cv:
|
|
119
134
|
self._call_result[call['id']] = {
|
|
120
135
|
'result': api_result,
|
|
121
136
|
'exception': api_exception
|
|
122
137
|
}
|
|
123
|
-
self._call_result_cv.
|
|
138
|
+
self._call_result_cv.notify_all()
|
|
124
139
|
|
|
125
140
|
def _get_call_id(self):
|
|
126
141
|
|
|
@@ -160,6 +175,20 @@ class Service:
|
|
|
160
175
|
|
|
161
176
|
return call_result['result']
|
|
162
177
|
|
|
178
|
+
def set_call_interval(self, call_interval: float):
|
|
179
|
+
"""設定 API 呼叫之間的最小時間間隔。
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
call_interval (float): 最小時間間隔(秒),必須 >= 0。
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
None
|
|
186
|
+
"""
|
|
187
|
+
check_value.check_type(call_interval, (int, float), 'call_interval')
|
|
188
|
+
if call_interval < 0:
|
|
189
|
+
raise exceptions.ParameterError('call_interval must be >= 0')
|
|
190
|
+
self._call_interval = call_interval
|
|
191
|
+
|
|
163
192
|
def close(self):
|
|
164
193
|
self.logger.info('close')
|
|
165
194
|
self._close = True
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
key = """-----BEGIN EC PARAMETERS-----
|
|
2
|
+
BggqhkjOPQMBBw==
|
|
3
|
+
-----END EC PARAMETERS-----
|
|
4
|
+
-----BEGIN EC PRIVATE KEY-----
|
|
5
|
+
MHcCAQEEIJ4+93aR3f/McywQGoGTaA4Yp0UkTsG/wnpTMHoRM6GyoAoGCCqGSM49
|
|
6
|
+
AwEHoUQDQgAEIBR8dqNw+UPQDb4TauXJAOiBKt6NtroEnTnugQOWNIoK68pZZFxJ
|
|
7
|
+
IlC1pAUKU5aPpihf5ciQx0vtMsKvwL9Q2Q==
|
|
8
|
+
-----END EC PRIVATE KEY-----
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
cert = """-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIB5zCCAY8CFH565JyYP4U6zJXXDS3k4g54XJ/iMAoGCCqGSM49BAMCMHcxCzAJ
|
|
13
|
+
BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
|
|
14
|
+
CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
|
|
15
|
+
EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA1MDkxNDIzMjVaFw0zNjA1MDYxNDIz
|
|
16
|
+
MjVaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
|
|
17
|
+
eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
|
|
18
|
+
bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
|
|
19
|
+
AwEHA0IABCAUfHajcPlD0A2+E2rlyQDogSrejba6BJ057oEDljSKCuvKWWRcSSJQ
|
|
20
|
+
taQFClOWj6YoX+XIkMdL7TLCr8C/UNkwCgYIKoZIzj0EAwIDRgAwQwIfbizXt/No
|
|
21
|
+
1GN3IfYB6qRwEZMJ8/AV31mWZve37j1g2AIgPQuPxE3QfgaxMsAmy6s0Xvs1dCRk
|
|
22
|
+
65bTnGMhK3zjpJE=
|
|
23
|
+
-----END CERTIFICATE-----
|
|
24
|
+
"""
|
|
@@ -17,12 +17,27 @@ def test_get_user(ptt_bots):
|
|
|
17
17
|
"""Tests getting user information from PTT."""
|
|
18
18
|
test_users = [
|
|
19
19
|
'CodingMan',
|
|
20
|
+
'codingman',
|
|
21
|
+
'CODINGMAN',
|
|
22
|
+
'cOdInGmAn',
|
|
20
23
|
]
|
|
21
24
|
|
|
22
|
-
for _ in range(
|
|
25
|
+
for _ in range(3):
|
|
23
26
|
for test_user in test_users:
|
|
24
27
|
user_info = ptt_bots[0].get_user(test_user)
|
|
25
28
|
_check_user_info(user_info, test_user)
|
|
26
29
|
|
|
27
|
-
time.sleep(1)
|
|
28
30
|
|
|
31
|
+
def test_get_user_no_such_user(ptt_bots):
|
|
32
|
+
"""Tests that querying a non-existent user raises NoSuchUser."""
|
|
33
|
+
import PyPtt
|
|
34
|
+
import pytest
|
|
35
|
+
|
|
36
|
+
no_such_users = [
|
|
37
|
+
'zzznotexist999',
|
|
38
|
+
'qqzzxxyy123',
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
for user in no_such_users:
|
|
42
|
+
with pytest.raises(PyPtt.exceptions.NoSuchUser):
|
|
43
|
+
ptt_bots[0].get_user(user)
|
|
@@ -5,6 +5,53 @@ import time
|
|
|
5
5
|
from PyPtt import Service, PostField
|
|
6
6
|
from tests import config # Assuming config.py has PTT1_ID and PTT1_PW
|
|
7
7
|
|
|
8
|
+
class TestCallInterval:
|
|
9
|
+
"""Tests for call_interval feature (no network required)."""
|
|
10
|
+
|
|
11
|
+
def test_default_call_interval(self):
|
|
12
|
+
service = Service()
|
|
13
|
+
assert service._call_interval == 0
|
|
14
|
+
service.close()
|
|
15
|
+
|
|
16
|
+
def test_init_with_call_interval(self):
|
|
17
|
+
service = Service(call_interval=1.5)
|
|
18
|
+
assert service._call_interval == 1.5
|
|
19
|
+
service.close()
|
|
20
|
+
|
|
21
|
+
def test_init_with_int_call_interval(self):
|
|
22
|
+
service = Service(call_interval=2)
|
|
23
|
+
assert service._call_interval == 2
|
|
24
|
+
service.close()
|
|
25
|
+
|
|
26
|
+
def test_set_call_interval(self):
|
|
27
|
+
service = Service()
|
|
28
|
+
service.set_call_interval(0.5)
|
|
29
|
+
assert service._call_interval == 0.5
|
|
30
|
+
service.set_call_interval(0)
|
|
31
|
+
assert service._call_interval == 0
|
|
32
|
+
service.close()
|
|
33
|
+
|
|
34
|
+
def test_negative_call_interval_init(self):
|
|
35
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
36
|
+
Service(call_interval=-1)
|
|
37
|
+
|
|
38
|
+
def test_negative_call_interval_setter(self):
|
|
39
|
+
service = Service()
|
|
40
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
41
|
+
service.set_call_interval(-1)
|
|
42
|
+
service.close()
|
|
43
|
+
|
|
44
|
+
def test_invalid_type_call_interval_init(self):
|
|
45
|
+
with pytest.raises(TypeError):
|
|
46
|
+
Service(call_interval='fast')
|
|
47
|
+
|
|
48
|
+
def test_invalid_type_call_interval_setter(self):
|
|
49
|
+
service = Service()
|
|
50
|
+
with pytest.raises(TypeError):
|
|
51
|
+
service.set_call_interval('fast')
|
|
52
|
+
service.close()
|
|
53
|
+
|
|
54
|
+
|
|
8
55
|
@pytest.fixture(scope="module", autouse=True)
|
|
9
56
|
def service_instance():
|
|
10
57
|
import time
|
pyptt-2.0.7/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
|
-
MHcCAQEEIFvkfPbkl4Ke99iXWTXG1LJnhFeMFREJWW9d4I76PZmGoAoGCCqGSM49
|
|
6
|
-
AwEHoUQDQgAErWNfoKcmEmi+0tNm2CCne7MhF0+4iKZYJvtrn5kxRG73XWhoR79c
|
|
7
|
-
7tfex7gLuFn/VXiP+rgDOe4MLT1LGrtD4A==
|
|
8
|
-
-----END EC PRIVATE KEY-----
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
cert = """-----BEGIN CERTIFICATE-----
|
|
12
|
-
MIIB6TCCAY8CFFzZNObjJZqIs69GFDYbSXJqNpBIMAoGCCqGSM49BAMCMHcxCzAJ
|
|
13
|
-
BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
|
|
14
|
-
CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
|
|
15
|
-
EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA0MDMxNDM1MzRaFw0zNjAzMzExNDM1
|
|
16
|
-
MzRaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
|
|
17
|
-
eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
|
|
18
|
-
bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
|
|
19
|
-
AwEHA0IABK1jX6CnJhJovtLTZtggp3uzIRdPuIimWCb7a5+ZMURu911oaEe/XO7X
|
|
20
|
-
3se4C7hZ/1V4j/q4AznuDC09Sxq7Q+AwCgYIKoZIzj0EAwIDSAAwRQIhAOAM8IPM
|
|
21
|
-
5D4BoaWYf3SaSVzrm8v6FtSIhWrSYJatYjwoAiAW3NggrC+qYY3wYRDq8UmaiHLv
|
|
22
|
-
pkJCEsbjl+remcwPbg==
|
|
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
|