PyPtt 2.2.0__tar.gz → 2.2.2__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.
Files changed (94) hide show
  1. {pyptt-2.2.0 → pyptt-2.2.2}/PKG-INFO +1 -1
  2. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/PTT.py +71 -0
  3. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/__init__.py +1 -1
  4. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_board_list.py +4 -1
  5. pyptt-2.2.2/PyPtt/_api_lottery.py +164 -0
  6. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/data_type.py +21 -0
  7. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/exceptions.py +8 -0
  8. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/lang_en_US.py +3 -0
  9. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/lang_zh_TW.py +3 -0
  10. pyptt-2.2.2/PyPtt/ssl_config.py +24 -0
  11. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt.egg-info/PKG-INFO +1 -1
  12. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt.egg-info/SOURCES.txt +2 -0
  13. pyptt-2.2.2/tests/test_lottery.py +92 -0
  14. pyptt-2.2.0/PyPtt/ssl_config.py +0 -24
  15. {pyptt-2.2.0 → pyptt-2.2.2}/LICENSE +0 -0
  16. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_bucket.py +0 -0
  17. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_call_status.py +0 -0
  18. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_change_pw.py +0 -0
  19. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_comment.py +0 -0
  20. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_del_post.py +0 -0
  21. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_board_info.py +0 -0
  22. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_bottom_post_list.py +0 -0
  23. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_favourite_board.py +0 -0
  24. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_newest_index.py +0 -0
  25. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_post.py +0 -0
  26. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_post_index.py +0 -0
  27. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_post_list.py +0 -0
  28. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_time.py +0 -0
  29. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_user.py +0 -0
  30. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_get_waterball.py +0 -0
  31. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_give_money.py +0 -0
  32. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_has_new_mail.py +0 -0
  33. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_loginout.py +0 -0
  34. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_mail.py +0 -0
  35. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_mark_post.py +0 -0
  36. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_post.py +0 -0
  37. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_reply_post.py +0 -0
  38. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_search_user.py +0 -0
  39. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_set_board_title.py +0 -0
  40. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/_api_util.py +0 -0
  41. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/api_server.py +0 -0
  42. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/check_value.py +0 -0
  43. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/command.py +0 -0
  44. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/config.py +0 -0
  45. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/connect_core.py +0 -0
  46. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/i18n.py +0 -0
  47. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/lib_util.py +0 -0
  48. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/log.py +0 -0
  49. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/screens.py +0 -0
  50. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt/service.py +0 -0
  51. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt.egg-info/dependency_links.txt +0 -0
  52. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt.egg-info/requires.txt +0 -0
  53. {pyptt-2.2.0 → pyptt-2.2.2}/PyPtt.egg-info/top_level.txt +0 -0
  54. {pyptt-2.2.0 → pyptt-2.2.2}/README.md +0 -0
  55. {pyptt-2.2.0 → pyptt-2.2.2}/pyproject.toml +0 -0
  56. {pyptt-2.2.0 → pyptt-2.2.2}/setup.cfg +0 -0
  57. {pyptt-2.2.0 → pyptt-2.2.2}/setup.py +0 -0
  58. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_bucket.py +0 -0
  59. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_change_pw.py +0 -0
  60. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_comment.py +0 -0
  61. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_del_post.py +0 -0
  62. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_del_post_unit.py +0 -0
  63. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_exceptions.py +0 -0
  64. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_fast_post.py +0 -0
  65. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_board_info.py +0 -0
  66. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_board_list.py +0 -0
  67. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_bottom_post_list.py +0 -0
  68. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_content_formula.py +0 -0
  69. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_favourite_boards.py +0 -0
  70. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_mail.py +0 -0
  71. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_newest_index.py +0 -0
  72. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_post.py +0 -0
  73. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_post_list.py +0 -0
  74. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_post_list_parser.py +0 -0
  75. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_post_parser.py +0 -0
  76. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_time.py +0 -0
  77. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_user.py +0 -0
  78. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_get_waterball.py +0 -0
  79. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_give_money.py +0 -0
  80. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_i18n.py +0 -0
  81. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_init.py +0 -0
  82. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_logger.py +0 -0
  83. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_login_logout.py +0 -0
  84. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_mail_send_and_del.py +0 -0
  85. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_mark_post.py +0 -0
  86. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_parse_query_post.py +0 -0
  87. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_post.py +0 -0
  88. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_post_anonymous.py +0 -0
  89. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_pyte_parity.py +0 -0
  90. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_reply_post.py +0 -0
  91. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_search_user.py +0 -0
  92. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_service.py +0 -0
  93. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_set_board_title.py +0 -0
  94. {pyptt-2.2.0 → pyptt-2.2.2}/tests/test_vt100_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: PyPtt github: https://github.com/PyPtt/PyPtt
5
5
  Author-email: CodingMan <pttcodingman@gmail.com>
6
6
  License: GNU Lesser General Public License v3 (LGPLv3)
@@ -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
@@ -601,6 +602,76 @@ class API:
601
602
 
602
603
  _api_give_money.give_money(self, ptt_id, money, red_bag_title, red_bag_content)
603
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
+
604
675
  def mail(self, ptt_id: str, title: str, content: str, sign_file: [int | str] = 0,
605
676
  backup: bool = True) -> None:
606
677
 
@@ -1,4 +1,4 @@
1
- __version__ = '2.2.0'
1
+ __version__ = '2.2.2'
2
2
  __author__ = 'CodingMan'
3
3
  __email__ = 'pttcodingman@gmail.com'
4
4
 
@@ -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
- pb.update(no)
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
+ }
@@ -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
+ MHcCAQEEIAZVg2x/IcFh54K0mjEMCMRBeLb1i0OVYCLCwDq4tDPMoAoGCCqGSM49
6
+ AwEHoUQDQgAEmXiHq0YNpgQ5xD36Dw3LdgqkltozAUj9C5CMZAHmOcYcxHtpcQhf
7
+ 2JA6z7ukZ43l5dy+08UJTPd5kRafC5zb3w==
8
+ -----END EC PRIVATE KEY-----
9
+ """
10
+
11
+ cert = """-----BEGIN CERTIFICATE-----
12
+ MIIB6TCCAY8CFC0c6T5NMH4N7g/BZUA1O8LT8Ku/MAoGCCqGSM49BAMCMHcxCzAJ
13
+ BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
14
+ CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
15
+ EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA3MTkxMjQ4NTVaFw0zNjA3MTYxMjQ4
16
+ NTVaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
17
+ eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
18
+ bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
19
+ AwEHA0IABJl4h6tGDaYEOcQ9+g8Ny3YKpJbaMwFI/QuQjGQB5jnGHMR7aXEIX9iQ
20
+ Os+7pGeN5eXcvtPFCUz3eZEWnwuc298wCgYIKoZIzj0EAwIDSAAwRQIhAPx2i8r9
21
+ hS7BlW5jpAKAQLOCi2BcDlk75o8UYsqXRFrKAiAUmszp1feqLXBRyBRMwf+VcXqf
22
+ kAlemyrmfv5YPHaq4g==
23
+ -----END CERTIFICATE-----
24
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPtt
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: PyPtt github: https://github.com/PyPtt/PyPtt
5
5
  Author-email: CodingMan <pttcodingman@gmail.com>
6
6
  License: GNU Lesser General Public License v3 (LGPLv3)
@@ -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,6 +77,7 @@ 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
@@ -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)
@@ -1,24 +0,0 @@
1
- key = """-----BEGIN EC PARAMETERS-----
2
- BggqhkjOPQMBBw==
3
- -----END EC PARAMETERS-----
4
- -----BEGIN EC PRIVATE KEY-----
5
- MHcCAQEEIK/vT3CLlx1CdJ6OlvrB70J+1+QzpKnb0DGYe9u1cWaboAoGCCqGSM49
6
- AwEHoUQDQgAEhzeNEtPu4xVRU7aGhl59oKVJZsZue2B/eEQkmme2gsMzzS6vGJKj
7
- 6mzkEmDNZv/DLFxccYesWd0comFxe/xOyA==
8
- -----END EC PRIVATE KEY-----
9
- """
10
-
11
- cert = """-----BEGIN CERTIFICATE-----
12
- MIIB5zCCAY8CFGe33bG5Ilg7iZcbrVQyTUOOD9uiMAoGCCqGSM49BAMCMHcxCzAJ
13
- BgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
14
- CgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRQw
15
- EgYDVQQDDAtDb21tb24gTmFtZTAeFw0yNjA3MTkxMjI2NDVaFw0zNjA3MTYxMjI2
16
- NDVaMHcxCzAJBgNVBAYTAlRXMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0
17
- eTEVMBMGA1UECgwMT3JnYW5pemF0aW9uMRwwGgYDVQQLDBNPcmdhbml6YXRpb25h
18
- bCBVbml0MRQwEgYDVQQDDAtDb21tb24gTmFtZTBZMBMGByqGSM49AgEGCCqGSM49
19
- AwEHA0IABIc3jRLT7uMVUVO2hoZefaClSWbGbntgf3hEJJpntoLDM80urxiSo+ps
20
- 5BJgzWb/wyxcXHGHrFndHKJhcXv8TsgwCgYIKoZIzj0EAwIDRgAwQwIfcZoiJkfz
21
- rUDQbdsoh6/7UsBnxSB/01+u6KIfZgepvwIgPT5JXHcIMPgo/dt5ph75qYq78GAN
22
- zVh3EBftVZSO8po=
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