PyPtt 2.0.7__tar.gz → 2.0.8__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.8}/PKG-INFO +1 -1
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/PTT.py +1 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/__init__.py +1 -1
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_bucket.py +1 -1
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_call_status.py +11 -8
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_comment.py +1 -1
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_board_info.py +5 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_user.py +18 -6
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_give_money.py +1 -2
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_mail.py +2 -2
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_mark_post.py +1 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_reply_post.py +2 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_util.py +7 -2
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/connect_core.py +17 -6
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/data_type.py +8 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/screens.py +3 -3
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/service.py +32 -3
- pyptt-2.0.8/PyPtt/ssl_config.py +24 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt.egg-info/PKG-INFO +1 -1
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_user.py +17 -2
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_service.py +47 -0
- pyptt-2.0.7/PyPtt/ssl_config.py +0 -24
- {pyptt-2.0.7 → pyptt-2.0.8}/LICENSE +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_change_pw.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_del_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_board_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_bottom_post_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_favourite_board.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_newest_index.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_post_index.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_post_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_time.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_get_waterball.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_has_new_mail.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_loginout.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_search_user.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/_api_set_board_title.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/api_server.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/check_value.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/command.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/config.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/exceptions.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/i18n.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/lang_en_US.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/lang_zh_TW.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/lib_util.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt/log.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt.egg-info/SOURCES.txt +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt.egg-info/dependency_links.txt +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt.egg-info/requires.txt +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/PyPtt.egg-info/top_level.txt +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/README.md +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/pyproject.toml +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/setup.cfg +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/setup.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_change_pw.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_comment.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_del_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_exceptions.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_board_info.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_board_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_bottom_post_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_favourite_boards.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_mail.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_newest_index.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_post_list.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_post_parser.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_time.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_get_waterball.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_give_money.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_i18n.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_init.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_logger.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_parse_query_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_reply_post.py +0 -0
- {pyptt-2.0.7 → pyptt-2.0.8}/tests/test_search_user.py +0 -0
|
@@ -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)
|
|
@@ -249,6 +249,8 @@ class API(object):
|
|
|
249
249
|
self._RDQ.add(screen)
|
|
250
250
|
if target == self._UseTooManyResources:
|
|
251
251
|
use_too_many_res = True
|
|
252
|
+
find_target = True
|
|
253
|
+
log.logger.info(i18n.use_too_many_resources)
|
|
252
254
|
break
|
|
253
255
|
target.raise_exception()
|
|
254
256
|
|
|
@@ -327,19 +329,22 @@ class API(object):
|
|
|
327
329
|
raise exceptions.UseTooManyResources()
|
|
328
330
|
raise exceptions.ConnectionClosed()
|
|
329
331
|
|
|
332
|
+
if isinstance(data_chunk, str):
|
|
333
|
+
data_chunk = data_chunk.encode('utf-8')
|
|
330
334
|
receive_data_buffer += data_chunk
|
|
331
335
|
|
|
332
336
|
screen, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
|
|
333
337
|
self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)
|
|
334
338
|
|
|
335
|
-
if
|
|
336
|
-
self.current_encoding
|
|
339
|
+
if not find_target:
|
|
340
|
+
original_encoding = self.current_encoding
|
|
341
|
+
self.current_encoding = 'big5uao' if original_encoding == 'utf-8' else 'utf-8'
|
|
337
342
|
screen_, find_target, is_secret, break_detect_after_send, use_too_many_res, msg, target_index = \
|
|
338
343
|
self._decode_screen(receive_data_buffer, start_time, target_list, is_secret, refresh, msg)
|
|
339
344
|
if find_target:
|
|
340
345
|
screen = screen_
|
|
341
346
|
else:
|
|
342
|
-
self.current_encoding =
|
|
347
|
+
self.current_encoding = original_encoding
|
|
343
348
|
|
|
344
349
|
if find_target:
|
|
345
350
|
break
|
|
@@ -348,13 +353,15 @@ class API(object):
|
|
|
348
353
|
vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding)
|
|
349
354
|
screens.show(self.config, vt100_p.screen)
|
|
350
355
|
self._RDQ.add(vt100_p.screen)
|
|
356
|
+
if use_too_many_res:
|
|
357
|
+
raise exceptions.UseTooManyResources()
|
|
351
358
|
return -1
|
|
352
359
|
|
|
353
360
|
if target_index != -1:
|
|
354
361
|
return target_index
|
|
355
362
|
|
|
356
363
|
if use_too_many_res:
|
|
357
|
-
|
|
364
|
+
raise exceptions.UseTooManyResources()
|
|
358
365
|
|
|
359
366
|
if not find_target:
|
|
360
367
|
return -1
|
|
@@ -366,7 +373,7 @@ class API(object):
|
|
|
366
373
|
raise exceptions.ParameterError('Item of TargetList must be TargetUnit')
|
|
367
374
|
|
|
368
375
|
if self._UseTooManyResources not in target_list:
|
|
369
|
-
target_list
|
|
376
|
+
target_list = target_list + [self._UseTooManyResources]
|
|
370
377
|
|
|
371
378
|
if self.config.connect_mode == data_type.ConnectMode.TELNET:
|
|
372
379
|
# Original Telnet logic remains, as it doesn't use asyncio
|
|
@@ -428,7 +435,11 @@ class API(object):
|
|
|
428
435
|
|
|
429
436
|
def close(self):
|
|
430
437
|
if self.config.connect_mode == data_type.ConnectMode.WEBSOCKETS:
|
|
431
|
-
|
|
438
|
+
try:
|
|
439
|
+
is_open = not self._core.closed
|
|
440
|
+
except AttributeError:
|
|
441
|
+
is_open = getattr(self._core, 'open', False)
|
|
442
|
+
if self._core and is_open:
|
|
432
443
|
loop = self._get_event_loop()
|
|
433
444
|
try:
|
|
434
445
|
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
|
+
MHcCAQEEIERB+UA+r+F9qEtBJ52f9p3sWAQkNIl0UZJv6O3vj3qtoAoGCCqGSM49
|
|
6
|
+
AwEHoUQDQgAEgTo7bZCEZpOdQa2o6P6KnSyVhfsFpvnEK0FGiU/V6xz6XWUMIVSy
|
|
7
|
+
+7dG5zGx7/OHa3iK6txCh11mPb+w/DYe8w==
|
|
8
|
+
-----END EC PRIVATE KEY-----
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
cert = """-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIB6TCCAY8CFCwmWlM+i2IbtHKF0gLrHa2/PqvPMAoGCCqGSM49BAMCMHcxCzAJ
|
|
13
|
+
BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
|
|
14
|
+
CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
|
|
15
|
+
EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA0MDUxNTIyNTZaFw0zNjA0MDIxNTIy
|
|
16
|
+
NTZaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
|
|
17
|
+
eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
|
|
18
|
+
bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
|
|
19
|
+
AwEHA0IABIE6O22QhGaTnUGtqOj+ip0slYX7Bab5xCtBRolP1esc+l1lDCFUsvu3
|
|
20
|
+
Rucxse/zh2t4iurcQoddZj2/sPw2HvMwCgYIKoZIzj0EAwIDSAAwRQIgc2K0N6II
|
|
21
|
+
gvywv1b/rDjIhQdXK6/TF62MWyk5GtrX7N0CIQCMyhwOeA+HYyh4yBbrbP5pA70b
|
|
22
|
+
n6j6snhIR6x2l0HWSw==
|
|
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
|
|
File without changes
|