PyPtt 2.1.7__tar.gz → 2.2.1__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.1.7 → pyptt-2.2.1}/PKG-INFO +1 -1
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/PTT.py +87 -2
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/__init__.py +1 -1
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_board_list.py +4 -1
- pyptt-2.2.1/PyPtt/_api_lottery.py +164 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_post.py +57 -2
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/data_type.py +21 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/exceptions.py +8 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/lang_en_US.py +3 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/lang_zh_TW.py +3 -0
- pyptt-2.2.1/PyPtt/ssl_config.py +24 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt.egg-info/PKG-INFO +1 -1
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt.egg-info/SOURCES.txt +3 -0
- pyptt-2.2.1/tests/test_lottery.py +92 -0
- pyptt-2.2.1/tests/test_post_anonymous.py +96 -0
- pyptt-2.1.7/PyPtt/ssl_config.py +0 -24
- {pyptt-2.1.7 → pyptt-2.2.1}/LICENSE +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_bucket.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_call_status.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_change_pw.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_comment.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_del_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_board_info.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_bottom_post_list.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_favourite_board.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_newest_index.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_post_index.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_post_list.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_time.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_user.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_get_waterball.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_give_money.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_has_new_mail.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_loginout.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_mail.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_mark_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_reply_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_search_user.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_set_board_title.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/_api_util.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/api_server.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/check_value.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/command.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/config.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/connect_core.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/i18n.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/lib_util.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/log.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/screens.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt/service.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt.egg-info/dependency_links.txt +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt.egg-info/requires.txt +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/PyPtt.egg-info/top_level.txt +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/README.md +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/pyproject.toml +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/setup.cfg +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/setup.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_bucket.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_change_pw.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_comment.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_del_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_del_post_unit.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_exceptions.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_fast_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_board_info.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_board_list.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_bottom_post_list.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_content_formula.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_favourite_boards.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_mail.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_newest_index.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_post_list.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_post_list_parser.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_post_parser.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_time.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_user.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_get_waterball.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_give_money.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_i18n.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_init.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_logger.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_login_logout.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_mail_send_and_del.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_mark_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_parse_query_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_pyte_parity.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_reply_post.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_search_user.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_service.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_set_board_title.py +0 -0
- {pyptt-2.1.7 → pyptt-2.2.1}/tests/test_vt100_parser.py +0 -0
|
@@ -21,6 +21,7 @@ from . import _api_get_user
|
|
|
21
21
|
from . import _api_give_money
|
|
22
22
|
from . import _api_loginout
|
|
23
23
|
from . import _api_get_waterball
|
|
24
|
+
from . import _api_lottery
|
|
24
25
|
from . import _api_mail
|
|
25
26
|
from . import _api_mark_post
|
|
26
27
|
from . import _api_post
|
|
@@ -436,7 +437,8 @@ class API:
|
|
|
436
437
|
return _api_get_newest_index.get_newest_index(
|
|
437
438
|
self, index_type, board, search_type, search_condition, search_list)
|
|
438
439
|
|
|
439
|
-
def post(self, board: str, title_index: int, title: str, content: str, sign_file: [str | int] = 0
|
|
440
|
+
def post(self, board: str, title_index: int, title: str, content: str, sign_file: [str | int] = 0,
|
|
441
|
+
anonymous: bool = False, display_id: Optional[str] = None) -> None:
|
|
440
442
|
"""
|
|
441
443
|
發文。
|
|
442
444
|
|
|
@@ -446,6 +448,12 @@ class API:
|
|
|
446
448
|
title (str): 文章標題。
|
|
447
449
|
content (str): 文章內容。
|
|
448
450
|
sign_file (str | int): 編號或隨機簽名檔 (x),預設為 0 (不選)。
|
|
451
|
+
anonymous (bool): 是否匿名發文,預設為 False。僅在該看板本身具有匿名 (BRD_ANONYMOUS)
|
|
452
|
+
屬性時才會生效,一般看板無法透過此參數強制匿名;PyPtt 只能在該提示出現時回覆它。
|
|
453
|
+
display_id (str): 匿名發文時欲顯示的自訂名稱;預設為 None,代表使用 pttbbs 預設值
|
|
454
|
+
(預設匿名板為 "Anonymous.",否則為原 ID)。pttbbs 會在暱名 (含預設的 "Anonymous")
|
|
455
|
+
後方加上一個 "."(例如 display_id='Fox' 會顯示為 "Fox."),此為 pttbbs 固定行為,
|
|
456
|
+
無法調整。僅能在 anonymous=True 時使用。
|
|
449
457
|
|
|
450
458
|
Returns:
|
|
451
459
|
None
|
|
@@ -455,6 +463,7 @@ class API:
|
|
|
455
463
|
RequireLogin: 需要登入。
|
|
456
464
|
NoSuchBoard: 看板不存在。
|
|
457
465
|
NoPermission: 沒有發佈權限。
|
|
466
|
+
ParameterError: display_id 於 anonymous=False 時提供。
|
|
458
467
|
|
|
459
468
|
範例::
|
|
460
469
|
|
|
@@ -464,13 +473,19 @@ class API:
|
|
|
464
473
|
try:
|
|
465
474
|
# .. login ..
|
|
466
475
|
ptt_bot.post(board='Test', title_index=1, title='PyPtt 程式貼文測試', content='測試內容', sign_file=0)
|
|
476
|
+
# 匿名發文 (需看板本身具有匿名屬性)
|
|
477
|
+
ptt_bot.post(board='AnonBoard', title_index=1, title='匿名貼文測試', content='測試內容',
|
|
478
|
+
sign_file=0, anonymous=True)
|
|
479
|
+
# 匿名發文並自訂顯示名稱
|
|
480
|
+
ptt_bot.post(board='AnonBoard', title_index=1, title='匿名貼文測試', content='測試內容',
|
|
481
|
+
sign_file=0, anonymous=True, display_id='Fox')
|
|
467
482
|
# .. do something ..
|
|
468
483
|
finally:
|
|
469
484
|
ptt_bot.logout()
|
|
470
485
|
|
|
471
486
|
"""
|
|
472
487
|
|
|
473
|
-
_api_post.post(self, board, title, content, title_index, sign_file)
|
|
488
|
+
_api_post.post(self, board, title, content, title_index, sign_file, anonymous, display_id)
|
|
474
489
|
|
|
475
490
|
def comment(self, board: str, comment_type: data_type.CommentType, content: str, aid: Optional[str] = None,
|
|
476
491
|
index: int = 0) -> None:
|
|
@@ -587,6 +602,76 @@ class API:
|
|
|
587
602
|
|
|
588
603
|
_api_give_money.give_money(self, ptt_id, money, red_bag_title, red_bag_content)
|
|
589
604
|
|
|
605
|
+
def get_lottery(self, board: str) -> Dict:
|
|
606
|
+
|
|
607
|
+
"""
|
|
608
|
+
取得看板樂透(彩券)目前的開獎資訊,對應看板內按 `f` 鍵的功能。
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
board (str): 看板名稱。
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Dict: 樂透資訊,包含 price(每張價格)、total(已下注總額)、
|
|
615
|
+
options(下注選項清單,每個選項包含 index、name、sold)。
|
|
616
|
+
|
|
617
|
+
Raises:
|
|
618
|
+
RequireLogin: 需要登入。
|
|
619
|
+
NoSuchBoard: 看板不存在。
|
|
620
|
+
NoSuchLottery: 該看板目前並沒有舉辦樂透,或樂透已封盤/開獎。
|
|
621
|
+
|
|
622
|
+
範例::
|
|
623
|
+
|
|
624
|
+
import PyPtt
|
|
625
|
+
|
|
626
|
+
ptt_bot = PyPtt.API()
|
|
627
|
+
try:
|
|
628
|
+
# .. login ..
|
|
629
|
+
lottery = ptt_bot.get_lottery(board='Test')
|
|
630
|
+
# .. do something ..
|
|
631
|
+
finally:
|
|
632
|
+
ptt_bot.logout()
|
|
633
|
+
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
return _api_lottery.get_lottery(self, board)
|
|
637
|
+
|
|
638
|
+
def bet_lottery(self, board: str, item: int = 1, amount: int = 1) -> Dict:
|
|
639
|
+
|
|
640
|
+
"""
|
|
641
|
+
在看板下注樂透(彩券),對應看板內按 `f` 鍵後購買彩券的功能。
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
board (str): 看板名稱。
|
|
645
|
+
item (int): 要購買的樂透選項編號,範圍 1~8(依看板實際選項數而定)。
|
|
646
|
+
amount (int): 購買張數,範圍 1~9999,並受限於帳戶內的 Ptt 幣餘額。
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
Dict: 下注結果,包含 board、item、name(選項名稱)、amount、
|
|
650
|
+
cost(本次下注花費的 Ptt 幣總額)。
|
|
651
|
+
|
|
652
|
+
Raises:
|
|
653
|
+
RequireLogin: 需要登入。
|
|
654
|
+
UnregisteredUser: 未註冊使用者。
|
|
655
|
+
NoSuchBoard: 看板不存在。
|
|
656
|
+
NoSuchLottery: 該看板目前並沒有舉辦樂透,或樂透已封盤/開獎。
|
|
657
|
+
NoMoney: 餘額不足。
|
|
658
|
+
|
|
659
|
+
範例::
|
|
660
|
+
|
|
661
|
+
import PyPtt
|
|
662
|
+
|
|
663
|
+
ptt_bot = PyPtt.API()
|
|
664
|
+
try:
|
|
665
|
+
# .. login ..
|
|
666
|
+
result = ptt_bot.bet_lottery(board='Test', item=1, amount=10)
|
|
667
|
+
# .. do something ..
|
|
668
|
+
finally:
|
|
669
|
+
ptt_bot.logout()
|
|
670
|
+
|
|
671
|
+
"""
|
|
672
|
+
|
|
673
|
+
return _api_lottery.bet_lottery(self, board, item, amount)
|
|
674
|
+
|
|
590
675
|
def mail(self, ptt_id: str, title: str, content: str, sign_file: [int | str] = 0,
|
|
591
676
|
backup: bool = True) -> None:
|
|
592
677
|
|
|
@@ -106,7 +106,10 @@ def get_board_list(api) -> list:
|
|
|
106
106
|
board_list.append(board_name)
|
|
107
107
|
|
|
108
108
|
if api.config.log_level == log.INFO:
|
|
109
|
-
|
|
109
|
+
# ponytail: max_no is parsed from the last-page screen and can
|
|
110
|
+
# undercount by one on some board sets; the bar is cosmetic, so
|
|
111
|
+
# clamp instead of letting pb.update raise and abort get_all_boards.
|
|
112
|
+
pb.update(min(no, max_no))
|
|
110
113
|
|
|
111
114
|
if no >= max_no:
|
|
112
115
|
break
|
|
@@ -0,0 +1,164 @@
|
|
|
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 lib_util
|
|
11
|
+
from . import log
|
|
12
|
+
from . import screens
|
|
13
|
+
from .data_type import LotteryBetField, LotteryField, LotteryOptionField
|
|
14
|
+
|
|
15
|
+
_price_pattern = re.compile(r'每張要花 (\d+)')
|
|
16
|
+
_total_pattern = re.compile(r'已下注總額: (\d+)')
|
|
17
|
+
_option_pattern = re.compile(r'(\d+)\.(.+?): *(\d+)')
|
|
18
|
+
|
|
19
|
+
# mbbsd/gamble.c: show_ticket_data()/join_gamble() vmsg() text for "no round
|
|
20
|
+
# open to bet on", either because none was ever held (show_ticket_data can't
|
|
21
|
+
# open ticket.items), or a running one's auto-close time already passed
|
|
22
|
+
# (join_gamble's stop_gamble() check, before ticket() is even entered).
|
|
23
|
+
_NO_LOTTERY_TEXTS = (
|
|
24
|
+
'目前並沒有舉辦樂透',
|
|
25
|
+
'目前未舉辦或樂透已開獎',
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_lottery_screen(board: str, screen: str) -> Dict:
|
|
30
|
+
price = 0
|
|
31
|
+
r = _price_pattern.search(screen)
|
|
32
|
+
if r is not None:
|
|
33
|
+
price = int(r.group(1))
|
|
34
|
+
|
|
35
|
+
total = 0
|
|
36
|
+
r = _total_pattern.search(screen)
|
|
37
|
+
if r is not None:
|
|
38
|
+
total = int(r.group(1))
|
|
39
|
+
|
|
40
|
+
# Options are only printed between these two headers (mbbsd/gamble.c:
|
|
41
|
+
# show_ticket_data()); slicing avoids the 站規 rule text above it, which
|
|
42
|
+
# can otherwise be mistaken for "N.name: number" (e.g. "2:00 11:00").
|
|
43
|
+
options = []
|
|
44
|
+
if '目前下注狀況' in screen and '已下注總額' in screen:
|
|
45
|
+
start = screen.index('目前下注狀況')
|
|
46
|
+
end = screen.index('已下注總額')
|
|
47
|
+
for m in _option_pattern.finditer(screen[start:end]):
|
|
48
|
+
options.append({
|
|
49
|
+
LotteryOptionField.index: int(m.group(1)),
|
|
50
|
+
LotteryOptionField.name: m.group(2).strip(),
|
|
51
|
+
LotteryOptionField.sold: int(m.group(3)),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
LotteryField.board: board,
|
|
56
|
+
LotteryField.price: price,
|
|
57
|
+
LotteryField.total: total,
|
|
58
|
+
LotteryField.options: options,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _enter_ticket_screen(api, board: str) -> str:
|
|
63
|
+
_api_util.goto_board(api, board)
|
|
64
|
+
|
|
65
|
+
target_list = [
|
|
66
|
+
connect_core.TargetUnit(
|
|
67
|
+
_NO_LOTTERY_TEXTS[0], break_detect=True, exceptions_=exceptions.NoSuchLottery(board)),
|
|
68
|
+
connect_core.TargetUnit(
|
|
69
|
+
_NO_LOTTERY_TEXTS[1], break_detect=True, exceptions_=exceptions.NoSuchLottery(board)),
|
|
70
|
+
connect_core.TargetUnit('請選擇要購買的種類', break_detect=True),
|
|
71
|
+
]
|
|
72
|
+
api.connect_core.send('f', target_list)
|
|
73
|
+
|
|
74
|
+
return api.connect_core.get_screen_queue()[-1]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _leave_ticket_screen(api) -> None:
|
|
78
|
+
target_list = [
|
|
79
|
+
connect_core.TargetUnit(screens.Target.InBoard, break_detect=True),
|
|
80
|
+
]
|
|
81
|
+
api.connect_core.send('q', target_list)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_lottery(api, board: str) -> Dict:
|
|
85
|
+
_api_util.one_thread(api)
|
|
86
|
+
|
|
87
|
+
if not api._is_login:
|
|
88
|
+
raise exceptions.RequireLogin(i18n.require_login)
|
|
89
|
+
|
|
90
|
+
check_value.check_type(board, str, 'board')
|
|
91
|
+
|
|
92
|
+
log.logger.info(i18n.replace(i18n.get_lottery, board))
|
|
93
|
+
|
|
94
|
+
ori_screen = _enter_ticket_screen(api, board)
|
|
95
|
+
result = _parse_lottery_screen(board, ori_screen)
|
|
96
|
+
|
|
97
|
+
_leave_ticket_screen(api)
|
|
98
|
+
|
|
99
|
+
log.logger.info(i18n.replace(i18n.get_lottery, board), '...', i18n.success)
|
|
100
|
+
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def bet_lottery(api, board: str, item: int = 1, amount: int = 1) -> Dict:
|
|
105
|
+
_api_util.one_thread(api)
|
|
106
|
+
|
|
107
|
+
if not api._is_login:
|
|
108
|
+
raise exceptions.RequireLogin(i18n.require_login)
|
|
109
|
+
|
|
110
|
+
if not api.is_registered_user:
|
|
111
|
+
raise exceptions.UnregisteredUser(lib_util.get_current_func_name())
|
|
112
|
+
|
|
113
|
+
check_value.check_type(board, str, 'board')
|
|
114
|
+
check_value.check_type(item, int, 'item')
|
|
115
|
+
check_value.check_range(item, 1, 8, 'item')
|
|
116
|
+
check_value.check_type(amount, int, 'amount')
|
|
117
|
+
check_value.check_range(amount, 1, 9999, 'amount')
|
|
118
|
+
|
|
119
|
+
log.logger.info(i18n.replace(i18n.bet_lottery, board))
|
|
120
|
+
|
|
121
|
+
ori_screen = _enter_ticket_screen(api, board)
|
|
122
|
+
before = _parse_lottery_screen(board, ori_screen)
|
|
123
|
+
|
|
124
|
+
options = before[LotteryField.options]
|
|
125
|
+
if item > len(options):
|
|
126
|
+
_leave_ticket_screen(api)
|
|
127
|
+
raise exceptions.ParameterError(
|
|
128
|
+
f'item {item} error, board {board} only has {len(options)} lottery options')
|
|
129
|
+
|
|
130
|
+
name = options[item - 1][LotteryOptionField.name]
|
|
131
|
+
price = before[LotteryField.price]
|
|
132
|
+
|
|
133
|
+
target_list = [
|
|
134
|
+
# buy_ticket_ui()'s getdata_str() pre-fills this field with "1" and
|
|
135
|
+
# parks the cursor after it (mbbsd/vtuikit.c: vgetstring() copies
|
|
136
|
+
# defstr into buf then sets icurr = iend = strlen(buf)); typing
|
|
137
|
+
# digits without clearing first inserts after the "1" (e.g. "3"
|
|
138
|
+
# becomes "13"), so Ctrl-Y (clear-to-start) must come first.
|
|
139
|
+
connect_core.TargetUnit(
|
|
140
|
+
'要買多少份呢', response=command.ctrl_y + str(amount) + command.enter),
|
|
141
|
+
connect_core.TargetUnit('現金不夠', break_detect=True, exceptions_=exceptions.NoMoney()),
|
|
142
|
+
connect_core.TargetUnit(
|
|
143
|
+
'板主已經停止下注了', break_detect=True, exceptions_=exceptions.NoSuchLottery(board)),
|
|
144
|
+
connect_core.TargetUnit('按任意鍵繼續', break_detect=True),
|
|
145
|
+
]
|
|
146
|
+
api.connect_core.send(str(item), target_list)
|
|
147
|
+
|
|
148
|
+
# dismiss the purchase-confirmation picture, back to the (refreshed) ticket screen
|
|
149
|
+
target_list = [
|
|
150
|
+
connect_core.TargetUnit('請選擇要購買的種類', break_detect=True),
|
|
151
|
+
]
|
|
152
|
+
api.connect_core.send(command.space, target_list)
|
|
153
|
+
|
|
154
|
+
_leave_ticket_screen(api)
|
|
155
|
+
|
|
156
|
+
log.logger.info(i18n.replace(i18n.bet_lottery, board), '...', i18n.success)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
LotteryBetField.board: board,
|
|
160
|
+
LotteryBetField.item: item,
|
|
161
|
+
LotteryBetField.name: name,
|
|
162
|
+
LotteryBetField.amount: amount,
|
|
163
|
+
LotteryBetField.cost: price * amount,
|
|
164
|
+
}
|
|
@@ -92,7 +92,49 @@ sign_file_list = [str(x) for x in range(0, 10)]
|
|
|
92
92
|
sign_file_list.append('x')
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
# --- post()'s `anonymous` / `display_id` params (issue #84) ---
|
|
96
|
+
#
|
|
97
|
+
# pttbbs only shows the "請輸入你想用的ID" / "確定[y/N]?" prompt pair when the
|
|
98
|
+
# *board itself* carries BRD_ANONYMOUS server-side (mbbsd/edit.c write_header,
|
|
99
|
+
# `if (currbrdattr & BRD_ANONYMOUS)`). On a normal board the two TargetUnits
|
|
100
|
+
# below simply never match anything on screen -- there is no way for PyPtt to
|
|
101
|
+
# force anonymity on a board that isn't flagged anonymous. See
|
|
102
|
+
# scripts/mkboard.py / scripts/bootstrap_local_pttbbs.py's AnonTest board for
|
|
103
|
+
# how to provision one locally.
|
|
104
|
+
#
|
|
105
|
+
# Response mapping, matching pttbbs' own rules for that prompt:
|
|
106
|
+
# anonymous=False -> 'r' (real userid; safe default, and a
|
|
107
|
+
# no-op on non-anonymous boards)
|
|
108
|
+
# anonymous=True, no display_id -> '' (Enter; "Anonymous." -- pttbbs
|
|
109
|
+
# appends the same trailing '.' as the
|
|
110
|
+
# display_id case below -- on a
|
|
111
|
+
# default-anonymous board, real userid
|
|
112
|
+
# otherwise -- pttbbs' own behavior)
|
|
113
|
+
# anonymous=True, display_id -> that string; pttbbs appends a trailing
|
|
114
|
+
# '.' to it (e.g. display_id='Fox' shows
|
|
115
|
+
# up as author 'Fox.') -- fixed pttbbs
|
|
116
|
+
# behavior, not adjustable by PyPtt.
|
|
117
|
+
def _anonymous_response(anonymous: bool, display_id: [str | None]) -> str:
|
|
118
|
+
if display_id is not None:
|
|
119
|
+
check_value.check_type(display_id, str, 'display_id')
|
|
120
|
+
if not anonymous:
|
|
121
|
+
raise exceptions.ParameterError('display_id requires anonymous=True')
|
|
122
|
+
# pttbbs (mbbsd/edit.c) treats a leading '-' as "this is a deleted
|
|
123
|
+
# name" and re-shows the same prompt instead of accepting it, which
|
|
124
|
+
# would make PyPtt's TargetUnit answer it the same way forever.
|
|
125
|
+
if display_id.startswith('-'):
|
|
126
|
+
raise exceptions.ParameterError("display_id must not start with '-'")
|
|
127
|
+
for c in display_id:
|
|
128
|
+
if ord(c) < 0x20 or ord(c) == 0x7f:
|
|
129
|
+
raise exceptions.ParameterError(
|
|
130
|
+
f'display_id must not contain control characters (found {c!r})')
|
|
131
|
+
return display_id
|
|
132
|
+
|
|
133
|
+
return '' if anonymous else 'r'
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def post(api, board: str, title: str, content: str, title_index: int, sign_file: [str | int],
|
|
137
|
+
anonymous: bool = False, display_id: [str | None] = None) -> None:
|
|
96
138
|
_api_util.one_thread(api)
|
|
97
139
|
|
|
98
140
|
if not api._is_login:
|
|
@@ -105,10 +147,13 @@ def post(api, board: str, title: str, content: str, title_index: int, sign_file:
|
|
|
105
147
|
check_value.check_type(title_index, int, 'title_index')
|
|
106
148
|
check_value.check_type(title, str, 'title')
|
|
107
149
|
check_value.check_type(content, str, 'content')
|
|
150
|
+
check_value.check_type(anonymous, bool, 'anonymous')
|
|
108
151
|
|
|
109
152
|
if str(sign_file).lower() not in sign_file_list:
|
|
110
153
|
raise exceptions.ParameterError(f'wrong parameter sign_file: {sign_file}')
|
|
111
154
|
|
|
155
|
+
anonymous_response = _anonymous_response(anonymous, display_id)
|
|
156
|
+
|
|
112
157
|
_api_util.check_board(api, board)
|
|
113
158
|
_api_util.goto_board(api, board)
|
|
114
159
|
|
|
@@ -147,7 +192,17 @@ def post(api, board: str, title: str, content: str, title_index: int, sign_file:
|
|
|
147
192
|
|
|
148
193
|
target_list = [
|
|
149
194
|
connect_core.TargetUnit('任意鍵繼續', break_detect=True),
|
|
150
|
-
|
|
195
|
+
# max_match=1: pttbbs' "檔案處理" screen keeps every answered prompt's
|
|
196
|
+
# echoed text on screen as the flow proceeds through the next ones
|
|
197
|
+
# (confirmed live -- "確定要儲存檔案嗎? s" stays visible through the
|
|
198
|
+
# anonymous-post prompts below). Without max_match=1 this target
|
|
199
|
+
# re-matches on every later screen in the same flow and re-sends
|
|
200
|
+
# 's' + Enter into whatever field currently has focus (e.g. the
|
|
201
|
+
# anonymous-ID or confirm prompt), corrupting the answer.
|
|
202
|
+
connect_core.TargetUnit('確定要儲存檔案嗎', response='s' + command.enter, max_match=1),
|
|
203
|
+
# Only shown on a BRD_ANONYMOUS board -- no-op on a normal board.
|
|
204
|
+
connect_core.TargetUnit('請輸入你想用的ID', response=anonymous_response + command.enter, max_match=1),
|
|
205
|
+
connect_core.TargetUnit('確定[y/N]', response='y' + command.enter, max_match=1),
|
|
151
206
|
connect_core.TargetUnit('x=隨機', response=str(sign_file) + command.enter),
|
|
152
207
|
]
|
|
153
208
|
api.connect_core.send(
|
|
@@ -219,6 +219,27 @@ class BoardField(AutoStrEnum):
|
|
|
219
219
|
post_kind_list = auto()
|
|
220
220
|
|
|
221
221
|
|
|
222
|
+
class LotteryField(AutoStrEnum):
|
|
223
|
+
board = auto()
|
|
224
|
+
price = auto()
|
|
225
|
+
total = auto()
|
|
226
|
+
options = auto()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class LotteryOptionField(AutoStrEnum):
|
|
230
|
+
index = auto()
|
|
231
|
+
name = auto()
|
|
232
|
+
sold = auto()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class LotteryBetField(AutoStrEnum):
|
|
236
|
+
board = auto()
|
|
237
|
+
item = auto()
|
|
238
|
+
name = auto()
|
|
239
|
+
amount = auto()
|
|
240
|
+
cost = auto()
|
|
241
|
+
|
|
242
|
+
|
|
222
243
|
class Compare(AutoStrEnum):
|
|
223
244
|
BIGGER = auto()
|
|
224
245
|
SAME = auto()
|
|
@@ -215,6 +215,14 @@ class NoSuchPost(Error):
|
|
|
215
215
|
return self.message
|
|
216
216
|
|
|
217
217
|
|
|
218
|
+
class NoSuchLottery(Error):
|
|
219
|
+
def __init__(self, board):
|
|
220
|
+
self.message = i18n.no_such_lottery + ': ' + board
|
|
221
|
+
|
|
222
|
+
def __str__(self):
|
|
223
|
+
return self.message
|
|
224
|
+
|
|
225
|
+
|
|
218
226
|
class CanNotUseSearchPostCode(Error):
|
|
219
227
|
"""
|
|
220
228
|
此狀態下無法使用搜尋文章代碼(AID)功能
|
|
@@ -5,6 +5,7 @@ string_data = {
|
|
|
5
5
|
"author": "Author",
|
|
6
6
|
"bad_post": "bad post",
|
|
7
7
|
"bad_post_not_recorded": "the post was deleted, but the bad-post record was not made",
|
|
8
|
+
"bet_lottery": "bet lottery _target0_",
|
|
8
9
|
"board": "Board",
|
|
9
10
|
"can_not_use_search_post_code_f": "This status can not use the search PostField code function",
|
|
10
11
|
"catch_bottom_post": "Catch bottom post",
|
|
@@ -31,6 +32,7 @@ string_data = {
|
|
|
31
32
|
"find_newest_index": "Find newest index",
|
|
32
33
|
"get_board_info": "Get board info _target0_",
|
|
33
34
|
"get_favourite_board_list": "Query favourite board list",
|
|
35
|
+
"get_lottery": "Get lottery info _target0_",
|
|
34
36
|
"get_mail": "Get mail",
|
|
35
37
|
"get_waterball": "Get waterball records",
|
|
36
38
|
"give_money_to": "give _target0_ _target1_ P coins",
|
|
@@ -76,6 +78,7 @@ string_data = {
|
|
|
76
78
|
"no_response": "This post has been closed and marked, no response",
|
|
77
79
|
"no_search_result": "No Search Result",
|
|
78
80
|
"no_such_board": "No such board",
|
|
81
|
+
"no_such_lottery": "No lottery is currently running, or betting is closed",
|
|
79
82
|
"no_such_mail": "No such mail",
|
|
80
83
|
"no_such_post": "In _target0_, the post code is not EXISTS _target1_",
|
|
81
84
|
"no_such_user": "No such user",
|
|
@@ -5,6 +5,7 @@ string_data = {
|
|
|
5
5
|
"author": "作者",
|
|
6
6
|
"bad_post": "劣文",
|
|
7
7
|
"bad_post_not_recorded": "文章已刪除,但退文(惡退)未成功記錄",
|
|
8
|
+
"bet_lottery": "下注樂透 _target0_",
|
|
8
9
|
"board": "看板",
|
|
9
10
|
"can_not_use_search_post_code_f": "此狀態下無法使用搜尋文章代碼(AID)功能",
|
|
10
11
|
"catch_bottom_post": "取得置底文章",
|
|
@@ -31,6 +32,7 @@ string_data = {
|
|
|
31
32
|
"find_newest_index": "找到最新編號",
|
|
32
33
|
"get_board_info": "取得看板資訊 _target0_",
|
|
33
34
|
"get_favourite_board_list": "取得我的最愛",
|
|
35
|
+
"get_lottery": "取得樂透資訊 _target0_",
|
|
34
36
|
"get_mail": "取得信件",
|
|
35
37
|
"get_waterball": "取得水球紀錄",
|
|
36
38
|
"give_money_to": "給 _target0_ _target1_ P 幣",
|
|
@@ -76,6 +78,7 @@ string_data = {
|
|
|
76
78
|
"no_response": "很抱歉, 此文章已結案並標記, 不得回應",
|
|
77
79
|
"no_search_result": "沒有搜尋結果",
|
|
78
80
|
"no_such_board": "無該看板",
|
|
81
|
+
"no_such_lottery": "目前並沒有舉辦樂透或已封盤",
|
|
79
82
|
"no_such_mail": "無此信件",
|
|
80
83
|
"no_such_post": "_target0_ 板找不到這個文章代碼 _target1_",
|
|
81
84
|
"no_such_user": "無該使用者",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
key = """-----BEGIN EC PARAMETERS-----
|
|
2
|
+
BggqhkjOPQMBBw==
|
|
3
|
+
-----END EC PARAMETERS-----
|
|
4
|
+
-----BEGIN EC PRIVATE KEY-----
|
|
5
|
+
MHcCAQEEIPR5AEs3hlT87YJb3cVPOjlrwtoi3mpIs4SutZ7I/czsoAoGCCqGSM49
|
|
6
|
+
AwEHoUQDQgAEYo0CaF+C4aiLepcNAH6B7LJjMmQ8kjzzr67E2IwniY9LTVYboifa
|
|
7
|
+
EefOhnSQNLHpyvoBhuxIa4Icnfy/bPEb5g==
|
|
8
|
+
-----END EC PRIVATE KEY-----
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
cert = """-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIB6TCCAY8CFHEfxTaKowfSEkiJOGonCzq3U3cJMAoGCCqGSM49BAMCMHcxCzAJ
|
|
13
|
+
BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
|
|
14
|
+
CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
|
|
15
|
+
EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA3MTkxMjM0MDdaFw0zNjA3MTYxMjM0
|
|
16
|
+
MDdaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
|
|
17
|
+
eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
|
|
18
|
+
bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
|
|
19
|
+
AwEHA0IABGKNAmhfguGoi3qXDQB+geyyYzJkPJI886+uxNiMJ4mPS01WG6In2hHn
|
|
20
|
+
zoZ0kDSx6cr6AYbsSGuCHJ38v2zxG+YwCgYIKoZIzj0EAwIDSAAwRQIhAOCb0At1
|
|
21
|
+
XW07h90zQWDcKcwK+Y7XPz3ajXeJey6F14xAAiBg4hiS77M5yR+AKndS4KB5Wo6j
|
|
22
|
+
HlWPOa4KFzY1Y+iXzw==
|
|
23
|
+
-----END CERTIFICATE-----
|
|
24
|
+
"""
|
|
@@ -23,6 +23,7 @@ PyPtt/_api_get_waterball.py
|
|
|
23
23
|
PyPtt/_api_give_money.py
|
|
24
24
|
PyPtt/_api_has_new_mail.py
|
|
25
25
|
PyPtt/_api_loginout.py
|
|
26
|
+
PyPtt/_api_lottery.py
|
|
26
27
|
PyPtt/_api_mail.py
|
|
27
28
|
PyPtt/_api_mark_post.py
|
|
28
29
|
PyPtt/_api_post.py
|
|
@@ -76,10 +77,12 @@ tests/test_i18n.py
|
|
|
76
77
|
tests/test_init.py
|
|
77
78
|
tests/test_logger.py
|
|
78
79
|
tests/test_login_logout.py
|
|
80
|
+
tests/test_lottery.py
|
|
79
81
|
tests/test_mail_send_and_del.py
|
|
80
82
|
tests/test_mark_post.py
|
|
81
83
|
tests/test_parse_query_post.py
|
|
82
84
|
tests/test_post.py
|
|
85
|
+
tests/test_post_anonymous.py
|
|
83
86
|
tests/test_pyte_parity.py
|
|
84
87
|
tests/test_reply_post.py
|
|
85
88
|
tests/test_search_user.py
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Integration tests for the per-board lottery (issue #95): get_lottery /
|
|
2
|
+
bet_lottery, driven against mbbsd/gamble.c's join_gamble()/ticket() flow (the
|
|
3
|
+
'f' key inside a board).
|
|
4
|
+
|
|
5
|
+
Round-driving tests need an actual open lottery round, which only the
|
|
6
|
+
LOCALHOST bootstrap provisions (board `PyPttLottery`, seeded directly via
|
|
7
|
+
`ticket.items` + a `ticket` marker file -- see
|
|
8
|
+
scripts/bootstrap_local_pttbbs.py, step_lottery()); they're skipped on the
|
|
9
|
+
real PTT hosts. item/amount range validation raises before any board
|
|
10
|
+
navigation, so those run everywhere.
|
|
11
|
+
"""
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
import PyPtt
|
|
15
|
+
from PyPtt import LotteryBetField, LotteryField, LotteryOptionField
|
|
16
|
+
|
|
17
|
+
LOTTERY_BOARD = 'PyPttLottery'
|
|
18
|
+
LOTTERY_PRICE = 10
|
|
19
|
+
LOTTERY_ITEMS = ('Alpha', 'Bravo', 'Charlie')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_get_lottery_seeded_round(ptt_bots):
|
|
23
|
+
"""get_lottery reads back the seeded price + option names + sold counts."""
|
|
24
|
+
ptt1_bot, ptt2_bot = ptt_bots
|
|
25
|
+
if ptt1_bot.host != PyPtt.HOST.LOCALHOST:
|
|
26
|
+
pytest.skip('PyPttLottery is only provisioned by the LOCALHOST bootstrap')
|
|
27
|
+
|
|
28
|
+
lottery = ptt2_bot.get_lottery(board=LOTTERY_BOARD)
|
|
29
|
+
|
|
30
|
+
assert lottery[LotteryField.board] == LOTTERY_BOARD
|
|
31
|
+
assert lottery[LotteryField.price] == LOTTERY_PRICE
|
|
32
|
+
|
|
33
|
+
options = lottery[LotteryField.options]
|
|
34
|
+
assert len(options) == len(LOTTERY_ITEMS)
|
|
35
|
+
assert [o[LotteryOptionField.name] for o in options] == list(LOTTERY_ITEMS)
|
|
36
|
+
assert [o[LotteryOptionField.index] for o in options] == [1, 2, 3]
|
|
37
|
+
for option in options:
|
|
38
|
+
assert option[LotteryOptionField.sold] >= 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_bet_lottery_increments_sold_count(ptt_bots):
|
|
42
|
+
"""Buying `amount` of an item bumps that item's sold count by exactly
|
|
43
|
+
`amount`, verified via a follow-up get_lottery -- pttbbs gives no
|
|
44
|
+
textual purchase receipt, so this is the ground truth."""
|
|
45
|
+
ptt1_bot, ptt2_bot = ptt_bots
|
|
46
|
+
if ptt1_bot.host != PyPtt.HOST.LOCALHOST:
|
|
47
|
+
pytest.skip('PyPttLottery is only provisioned by the LOCALHOST bootstrap')
|
|
48
|
+
|
|
49
|
+
item = 2
|
|
50
|
+
amount = 3
|
|
51
|
+
|
|
52
|
+
before = ptt2_bot.get_lottery(board=LOTTERY_BOARD)
|
|
53
|
+
sold_before = before[LotteryField.options][item - 1][LotteryOptionField.sold]
|
|
54
|
+
|
|
55
|
+
result = ptt2_bot.bet_lottery(board=LOTTERY_BOARD, item=item, amount=amount)
|
|
56
|
+
assert result[LotteryBetField.board] == LOTTERY_BOARD
|
|
57
|
+
assert result[LotteryBetField.item] == item
|
|
58
|
+
assert result[LotteryBetField.name] == LOTTERY_ITEMS[item - 1]
|
|
59
|
+
assert result[LotteryBetField.amount] == amount
|
|
60
|
+
assert result[LotteryBetField.cost] == LOTTERY_PRICE * amount
|
|
61
|
+
|
|
62
|
+
after = ptt2_bot.get_lottery(board=LOTTERY_BOARD)
|
|
63
|
+
sold_after = after[LotteryField.options][item - 1][LotteryOptionField.sold]
|
|
64
|
+
|
|
65
|
+
assert sold_after == sold_before + amount
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_get_lottery_no_round_raises(ptt_bots):
|
|
69
|
+
"""A board that never had a lottery round raises NoSuchLottery."""
|
|
70
|
+
ptt1_bot, ptt2_bot = ptt_bots
|
|
71
|
+
if ptt1_bot.host != PyPtt.HOST.LOCALHOST:
|
|
72
|
+
pytest.skip('needs a board known to never have hosted a lottery round')
|
|
73
|
+
|
|
74
|
+
with pytest.raises(PyPtt.exceptions.NoSuchLottery):
|
|
75
|
+
ptt2_bot.get_lottery(board='Test')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize('item', [0, 9])
|
|
79
|
+
def test_bet_lottery_invalid_item_raises(ptt_bots, item):
|
|
80
|
+
"""item is 1..8; out-of-range raises before any board navigation."""
|
|
81
|
+
ptt1_bot, ptt2_bot = ptt_bots
|
|
82
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
83
|
+
ptt2_bot.bet_lottery(board='Test', item=item, amount=1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.parametrize('amount', [0, 10000])
|
|
87
|
+
def test_bet_lottery_invalid_amount_raises(ptt_bots, amount):
|
|
88
|
+
"""amount is 1..9999 (pttbbs' getdata_str buffer is 4 digits); out-of-range
|
|
89
|
+
raises before any board navigation."""
|
|
90
|
+
ptt1_bot, ptt2_bot = ptt_bots
|
|
91
|
+
with pytest.raises(PyPtt.exceptions.ParameterError):
|
|
92
|
+
ptt2_bot.bet_lottery(board='Test', item=1, amount=amount)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Tests PyPtt issue #84 (匿名發文): post()'s anonymous/display_id params
|
|
2
|
+
against a board that actually carries pttbbs' BRD_ANONYMOUS/BRD_DEFAULTANONYMOUS
|
|
3
|
+
flags. Only pttbbs boards flagged this way ever show the anonymous prompt --
|
|
4
|
+
PyPtt cannot force anonymity on a normal board (see PyPtt/_api_post.py). Only
|
|
5
|
+
scripts/bootstrap_local_pttbbs.py's local container provisions such a board
|
|
6
|
+
(AnonTest), so this file is LOCALHOST-only.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
import PyPtt
|
|
15
|
+
from PyPtt import PostField, NewIndex
|
|
16
|
+
|
|
17
|
+
if os.environ.get('PTT_HOST') != 'LOCALHOST':
|
|
18
|
+
pytest.skip(
|
|
19
|
+
'test_post_anonymous.py needs the AnonTest board, which only '
|
|
20
|
+
'scripts/bootstrap_local_pttbbs.py provisions. Set PTT_HOST=LOCALHOST to run.',
|
|
21
|
+
allow_module_level=True)
|
|
22
|
+
|
|
23
|
+
ANON_BOARD = 'AnonTest'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _find_own_post(ptt_bot, title_marker):
|
|
27
|
+
"""Scan back from the newest post on ANON_BOARD for the one whose title
|
|
28
|
+
contains title_marker, and return its parsed post dict."""
|
|
29
|
+
newest_index = ptt_bot.get_newest_index(index_type=NewIndex.BOARD, board=ANON_BOARD)
|
|
30
|
+
for i in range(10):
|
|
31
|
+
index = newest_index - i
|
|
32
|
+
if index <= 0:
|
|
33
|
+
break
|
|
34
|
+
post = ptt_bot.get_post(board=ANON_BOARD, index=index)
|
|
35
|
+
if title_marker in post[PostField.title]:
|
|
36
|
+
return post
|
|
37
|
+
raise AssertionError(f'could not find a post titled with marker {title_marker!r}')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_post_anonymous(ptt_bots):
|
|
41
|
+
"""anonymous=True, display_id=None -> pttbbs masks the author as
|
|
42
|
+
'Anonymous.' (AnonTest is bootstrapped BRD_DEFAULTANONYMOUS, so an empty
|
|
43
|
+
response to the anon prompt means 'Anonymous', not the real id; pttbbs
|
|
44
|
+
appends a trailing '.' the same way it does for a custom display_id)."""
|
|
45
|
+
ptt_bot = ptt_bots[0]
|
|
46
|
+
marker = uuid.uuid4().hex[:12]
|
|
47
|
+
title = f'PyPtt anon test {marker}'
|
|
48
|
+
|
|
49
|
+
ptt_bot.post(board=ANON_BOARD, title_index=1, title=title,
|
|
50
|
+
content='PyPtt anonymous post test content.\n', sign_file=0,
|
|
51
|
+
anonymous=True)
|
|
52
|
+
|
|
53
|
+
time.sleep(1)
|
|
54
|
+
post = _find_own_post(ptt_bot, marker)
|
|
55
|
+
|
|
56
|
+
author = post[PostField.author]
|
|
57
|
+
assert author.split(' ')[0] == 'Anonymous.', f'expected masked author "Anonymous.", got {author!r}'
|
|
58
|
+
assert ptt_bot.ptt_id not in author, f'real ptt_id leaked into author field: {author!r}'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_post_anonymous_with_display_id(ptt_bots):
|
|
62
|
+
"""anonymous=True, display_id='PyPttFox' -> pttbbs masks the author as
|
|
63
|
+
the custom name plus a trailing '.' (pttbbs behavior, not adjustable)."""
|
|
64
|
+
ptt_bot = ptt_bots[0]
|
|
65
|
+
marker = uuid.uuid4().hex[:12]
|
|
66
|
+
title = f'PyPtt anon custom id test {marker}'
|
|
67
|
+
display_id = 'PyPttFox'
|
|
68
|
+
|
|
69
|
+
ptt_bot.post(board=ANON_BOARD, title_index=1, title=title,
|
|
70
|
+
content='PyPtt anonymous post (custom display id) test content.\n', sign_file=0,
|
|
71
|
+
anonymous=True, display_id=display_id)
|
|
72
|
+
|
|
73
|
+
time.sleep(1)
|
|
74
|
+
post = _find_own_post(ptt_bot, marker)
|
|
75
|
+
|
|
76
|
+
author = post[PostField.author]
|
|
77
|
+
assert author.split(' ')[0] == f'{display_id}.', f'expected masked author "{display_id}.", got {author!r}'
|
|
78
|
+
assert ptt_bot.ptt_id not in author, f'real ptt_id leaked into author field: {author!r}'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_post_not_anonymous_shows_real_id(ptt_bots):
|
|
82
|
+
"""anonymous=False (default) -> even on an anonymous-capable board, the
|
|
83
|
+
real ptt_id is used (safe default, backward compatible with pre-#84
|
|
84
|
+
callers of post())."""
|
|
85
|
+
ptt_bot = ptt_bots[0]
|
|
86
|
+
marker = uuid.uuid4().hex[:12]
|
|
87
|
+
title = f'PyPtt non-anon test {marker}'
|
|
88
|
+
|
|
89
|
+
ptt_bot.post(board=ANON_BOARD, title_index=1, title=title,
|
|
90
|
+
content='PyPtt non-anonymous post test content.\n', sign_file=0)
|
|
91
|
+
|
|
92
|
+
time.sleep(1)
|
|
93
|
+
post = _find_own_post(ptt_bot, marker)
|
|
94
|
+
|
|
95
|
+
author = post[PostField.author]
|
|
96
|
+
assert author.split(' ')[0] == ptt_bot.ptt_id, f'expected real author {ptt_bot.ptt_id!r}, got {author!r}'
|
pyptt-2.1.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
|
-
MHcCAQEEIBl6n84gGhyoI6rh8jsDM6+MvbtX++1SctY8mkfAU/PqoAoGCCqGSM49
|
|
6
|
-
AwEHoUQDQgAEKrKMWokWd4Y6dFHFFr4pg9h340NKkekdCrYbxv1l2SUBUDkXUx28
|
|
7
|
-
fGqtXxYCuOsEQTeQrm0OmCq6U+aRXMvwaQ==
|
|
8
|
-
-----END EC PRIVATE KEY-----
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
cert = """-----BEGIN CERTIFICATE-----
|
|
12
|
-
MIIB6DCCAY8CFEXMlgDToWo6s00YgTOitW0BgNCdMAoGCCqGSM49BAMCMHcxCzAJ
|
|
13
|
-
BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
|
|
14
|
-
CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
|
|
15
|
-
EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA3MTgwNDE0NTZaFw0zNjA3MTUwNDE0
|
|
16
|
-
NTZaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
|
|
17
|
-
eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
|
|
18
|
-
bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
|
|
19
|
-
AwEHA0IABCqyjFqJFneGOnRRxRa+KYPYd+NDSpHpHQq2G8b9ZdklAVA5F1MdvHxq
|
|
20
|
-
rV8WArjrBEE3kK5tDpgqulPmkVzL8GkwCgYIKoZIzj0EAwIDRwAwRAIgcfQdHGfL
|
|
21
|
-
m1j+zvm23ToVfbfGdlvcR4hXBQkWMQslAkICIEccC2jqieHYgaLRUgzoUVI/twm3
|
|
22
|
-
5uAjX0ZWPxTPuNvA
|
|
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
|
|
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
|