PyPtt 2.0.6__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.6 → pyptt-2.0.8}/PKG-INFO +1 -1
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/PTT.py +1 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/__init__.py +1 -1
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_bucket.py +1 -1
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_call_status.py +11 -8
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_comment.py +1 -1
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_board_info.py +5 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_user.py +42 -13
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_waterball.py +10 -2
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_give_money.py +1 -2
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_mail.py +2 -2
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_mark_post.py +1 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_reply_post.py +2 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_util.py +7 -2
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/connect_core.py +17 -6
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/data_type.py +8 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/screens.py +5 -3
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/service.py +32 -3
- pyptt-2.0.8/PyPtt/ssl_config.py +24 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt.egg-info/PKG-INFO +1 -1
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_user.py +17 -2
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_service.py +47 -0
- pyptt-2.0.6/PyPtt/ssl_config.py +0 -24
- {pyptt-2.0.6 → pyptt-2.0.8}/LICENSE +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_change_pw.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_del_post.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_board_list.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_bottom_post_list.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_favourite_board.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_newest_index.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_post.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_post_index.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_post_list.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_get_time.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_has_new_mail.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_loginout.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_post.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_search_user.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/_api_set_board_title.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/api_server.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/check_value.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/command.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/config.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/exceptions.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/i18n.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/lang_en_US.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/lang_zh_TW.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/lib_util.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt/log.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt.egg-info/SOURCES.txt +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt.egg-info/dependency_links.txt +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt.egg-info/requires.txt +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/PyPtt.egg-info/top_level.txt +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/README.md +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/pyproject.toml +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/setup.cfg +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/setup.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_change_pw.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_comment.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_del_post.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_exceptions.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_board_info.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_board_list.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_bottom_post_list.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_favourite_boards.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_mail.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_newest_index.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_post.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_post_list.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_post_parser.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_time.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_get_waterball.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_give_money.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_i18n.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_init.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_logger.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_parse_query_post.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_post.py +0 -0
- {pyptt-2.0.6 → pyptt-2.0.8}/tests/test_reply_post.py +0 -0
- {pyptt-2.0.6 → 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
|
|
@@ -76,7 +85,13 @@ def get_user(api, ptt_id: str) -> Dict:
|
|
|
76
85
|
|
|
77
86
|
# 《個人名片》CodingMan 目前沒有名片
|
|
78
87
|
|
|
79
|
-
lines = ori_screen.split('\n')
|
|
88
|
+
lines = ori_screen.split('\n')
|
|
89
|
+
|
|
90
|
+
def find_line(marker):
|
|
91
|
+
for i, line in enumerate(lines):
|
|
92
|
+
if marker in line:
|
|
93
|
+
return i, line
|
|
94
|
+
return -1, ''
|
|
80
95
|
|
|
81
96
|
def parse_user_info_from_line(line: str) -> (str, str):
|
|
82
97
|
part_0 = line[line.find('》') + 1:]
|
|
@@ -86,9 +101,20 @@ def get_user(api, ptt_id: str) -> Dict:
|
|
|
86
101
|
|
|
87
102
|
return part_0, part_1
|
|
88
103
|
|
|
89
|
-
|
|
104
|
+
id_idx, id_line = find_line('《ID暱稱》')
|
|
105
|
+
login_idx, login_line = find_line('《登入次數》')
|
|
106
|
+
activity_idx, activity_line = find_line('《目前動態》')
|
|
107
|
+
last_login_idx, last_login_line = find_line('《上次上站》')
|
|
108
|
+
chess_idx, chess_line = find_line('《 五子棋 》')
|
|
109
|
+
|
|
110
|
+
if any(idx == -1 for idx in [id_idx, login_idx, activity_idx, last_login_idx, chess_idx]):
|
|
111
|
+
log.logger.debug('get_user_parse_error', ori_screen)
|
|
112
|
+
raise exceptions.UnknownError(f'get_user parse error: {ori_screen}')
|
|
113
|
+
|
|
114
|
+
ptt_id, buff_1 = parse_user_info_from_line(id_line)
|
|
90
115
|
money = int(int_list[0]) if len(int_list := re.findall(r'\d+', buff_1)) > 0 else buff_1
|
|
91
|
-
|
|
116
|
+
|
|
117
|
+
buff_0, buff_1 = parse_user_info_from_line(login_line)
|
|
92
118
|
|
|
93
119
|
login_count = int(re.findall(r'\d+', buff_0)[0])
|
|
94
120
|
account_verified = ('同天內只計一次' in buff_0)
|
|
@@ -96,15 +122,16 @@ def get_user(api, ptt_id: str) -> Dict:
|
|
|
96
122
|
|
|
97
123
|
# PTT2 沒有退文
|
|
98
124
|
if api.config.host == data_type.HOST.PTT1:
|
|
99
|
-
|
|
125
|
+
int_list = re.findall(r'\d+', buff_1)
|
|
126
|
+
illegal_post = int(int_list[1]) if len(int_list) > 1 else 0
|
|
100
127
|
else:
|
|
101
128
|
illegal_post = None
|
|
102
129
|
|
|
103
|
-
activity, mail = parse_user_info_from_line(
|
|
104
|
-
last_login_date, last_login_ip = parse_user_info_from_line(
|
|
105
|
-
five_chess, chess = parse_user_info_from_line(
|
|
130
|
+
activity, mail = parse_user_info_from_line(activity_line)
|
|
131
|
+
last_login_date, last_login_ip = parse_user_info_from_line(last_login_line)
|
|
132
|
+
five_chess, chess = parse_user_info_from_line(chess_line)
|
|
106
133
|
|
|
107
|
-
signature_file = '\n'.join(lines[
|
|
134
|
+
signature_file = '\n'.join(lines[chess_idx + 1:-1]).strip('\n')
|
|
108
135
|
|
|
109
136
|
log.logger.debug('ptt_id', ptt_id)
|
|
110
137
|
log.logger.debug('money', money)
|
|
@@ -135,5 +162,7 @@ def get_user(api, ptt_id: str) -> Dict:
|
|
|
135
162
|
UserField.chess: chess,
|
|
136
163
|
UserField.signature_file: signature_file,
|
|
137
164
|
}
|
|
165
|
+
api._exist_user_list.add(ptt_id.lower())
|
|
166
|
+
|
|
138
167
|
user = json.dumps(user, cls=AutoJsonEncoder)
|
|
139
168
|
return json.loads(user)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
from datetime import datetime
|
|
4
5
|
from typing import List, Dict
|
|
5
6
|
|
|
6
7
|
from . import _api_util
|
|
@@ -19,6 +20,11 @@ _to_target_pattern = re.compile(r'To (\S+):')
|
|
|
19
20
|
_from_target_pattern = re.compile(r'★(\S+) ')
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def _parse_date(date_str: str) -> datetime:
|
|
24
|
+
"""Parse waterball date string into datetime object."""
|
|
25
|
+
return datetime.strptime(date_str, '%m/%d/%Y %H:%M:%S')
|
|
26
|
+
|
|
27
|
+
|
|
22
28
|
def _merge_continuation_lines(lines: list) -> list:
|
|
23
29
|
merged = []
|
|
24
30
|
i = 0
|
|
@@ -52,7 +58,7 @@ def _parse_waterball_line(line: str) -> Dict | None:
|
|
|
52
58
|
WaterballField.type: WaterballType.SEND,
|
|
53
59
|
WaterballField.target: target,
|
|
54
60
|
WaterballField.content: content,
|
|
55
|
-
WaterballField.date: date,
|
|
61
|
+
WaterballField.date: _parse_date(date),
|
|
56
62
|
}
|
|
57
63
|
elif line.startswith('★'):
|
|
58
64
|
target_match = _from_target_pattern.search(line)
|
|
@@ -66,7 +72,7 @@ def _parse_waterball_line(line: str) -> Dict | None:
|
|
|
66
72
|
WaterballField.type: WaterballType.CATCH,
|
|
67
73
|
WaterballField.target: target,
|
|
68
74
|
WaterballField.content: content,
|
|
69
|
-
WaterballField.date: date,
|
|
75
|
+
WaterballField.date: _parse_date(date),
|
|
70
76
|
}
|
|
71
77
|
return None
|
|
72
78
|
|
|
@@ -128,6 +134,8 @@ def get_waterball(api, post_action: WaterballPostAction = WaterballPostAction.KE
|
|
|
128
134
|
target_list_exit = [
|
|
129
135
|
connect_core.TargetUnit(screens.Target.WaterBallPostAction, log_level=log.DEBUG,
|
|
130
136
|
response=action_cmd + command.enter),
|
|
137
|
+
connect_core.TargetUnit(screens.Target.WaterballCleanup, log_level=log.DEBUG,
|
|
138
|
+
response='y' + command.enter),
|
|
131
139
|
connect_core.TargetUnit(screens.Target.MainMenu, log_level=log.DEBUG, break_detect=True),
|
|
132
140
|
]
|
|
133
141
|
api.connect_core.send('q', target_list_exit)
|
|
@@ -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()
|
|
@@ -104,6 +104,8 @@ class Target:
|
|
|
104
104
|
|
|
105
105
|
AnyKey = '任意鍵'
|
|
106
106
|
|
|
107
|
+
WaterballCleanup = '確定清除?'
|
|
108
|
+
|
|
107
109
|
InTalk = [
|
|
108
110
|
'【聊天說話】',
|
|
109
111
|
'線上使用者列表',
|
|
@@ -149,7 +151,7 @@ class Target:
|
|
|
149
151
|
]
|
|
150
152
|
|
|
151
153
|
use_too_many_resources = [
|
|
152
|
-
'
|
|
154
|
+
'程式耗用過多'
|
|
153
155
|
]
|
|
154
156
|
|
|
155
157
|
Animation = [
|
|
@@ -215,11 +217,11 @@ class VT100Parser:
|
|
|
215
217
|
|
|
216
218
|
def _move(self, x, y):
|
|
217
219
|
self._cursor_x = x
|
|
218
|
-
self._cursor_y = y
|
|
220
|
+
self._cursor_y = min(y, len(self.screen) - 1)
|
|
219
221
|
|
|
220
222
|
def _newline(self):
|
|
221
223
|
self._cursor_x = 0
|
|
222
|
-
self._cursor_y
|
|
224
|
+
self._cursor_y = min(self._cursor_y + 1, len(self.screen) - 1)
|
|
223
225
|
|
|
224
226
|
def _k(self):
|
|
225
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.6/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
|
-
MHcCAQEEIFnari/tIK4DobE9n6Q66VNwZ3iIMO4OIfYlxriiiiO2oAoGCCqGSM49
|
|
6
|
-
AwEHoUQDQgAEPB+56uUrwmG6uqxqvRWA/Q3qEMYcmyw4c/KgEKLN8IpzzH8oqfrc
|
|
7
|
-
94C1WVZ6w/VBePPn8NG4EOTSEzJoMEHKkQ==
|
|
8
|
-
-----END EC PRIVATE KEY-----
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
cert = """-----BEGIN CERTIFICATE-----
|
|
12
|
-
MIIB6jCCAY8CFAqMFEZkRx5MTexvhJ87UaaYKbmNMAoGCCqGSM49BAMCMHcxCzAJ
|
|
13
|
-
BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
|
|
14
|
-
CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
|
|
15
|
-
EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA0MDMwMzE5MjhaFw0zNjAzMzEwMzE5
|
|
16
|
-
MjhaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
|
|
17
|
-
eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
|
|
18
|
-
bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
|
|
19
|
-
AwEHA0IABDwfuerlK8Jhurqsar0VgP0N6hDGHJssOHPyoBCizfCKc8x/KKn63PeA
|
|
20
|
-
tVlWesP1QXjz5/DRuBDk0hMyaDBBypEwCgYIKoZIzj0EAwIDSQAwRgIhAImZ1AX9
|
|
21
|
-
SW/lm0nyJMeAgtOm29LosMscrNNqoGz50Ha+AiEAxSLRceZ4WocAWxMNXXLqbRq5
|
|
22
|
-
FDyqLTDZ4WFPLAGhhkY=
|
|
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
|