diff --git a/src/_version.py b/src/_version.py index 493f741..260c070 100644 --- a/src/_version.py +++ b/src/_version.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/src/api_ptt.py b/src/api_ptt.py index fa56133..bd74151 100644 --- a/src/api_ptt.py +++ b/src/api_ptt.py @@ -152,6 +152,7 @@ def login() -> Dict[str, Any]: - 'NO_SUCH_POST': 在看板中找不到文章 AID 或 Index。 - 'NO_PERMISSION': 沒有權限。 - 'LOGIN_FAILED': 登入失敗。 + - 'TWO_FACTOR_AUTH_REQUIRED': 需要二階段驗證,請先用一般 BBS 客戶端從這台機器手動登入完成驗證。 - 'WRONG_CREDENTIALS': 帳號或密碼錯誤。 - 'CANT_RESPONSE': 已結案並標記, 不得回應。 - 'NO_FAST_COMMENT': 推文間隔太短。 diff --git a/src/test_utils.py b/src/test_utils.py index 3b392ab..3f5b464 100644 --- a/src/test_utils.py +++ b/src/test_utils.py @@ -42,6 +42,51 @@ def test_parameter_error_maps_to_parameter_error_code(): sys.modules[m] = mod +def test_two_factor_auth_required_maps_to_dedicated_code(): + src_dir = os.path.dirname(os.path.abspath(__file__)) + if src_dir not in sys.path: + sys.path.insert(0, src_dir) + snapshot = {m: sys.modules.get(m) for m in _SHARED} + try: + if not getattr(sys.modules.get("PyPtt"), "__file__", None): + for m in _SHARED: + sys.modules.pop(m, None) + import PyPtt + + if not hasattr(PyPtt, "TwoFactorAuthRequired"): + pytest.skip("real PyPtt (>=2.3.6) not installed") + import utils + + # PyPtt 例外的 message 來自 i18n,建構前得先 init,否則 + # 存取 .message 會噴 AttributeError(PyPtt.API 平常會幫你做這步)。 + import PyPtt.data_type as pyptt_data_type + import PyPtt.i18n as pyptt_i18n + + pyptt_i18n.init(pyptt_data_type.Language.MANDARIN) + + # TwoFactorAuthRequired 繼承 LoginError,須確認沒被父類搶先攔截, + # 且保留 PyPtt 的原始訊息(不是被換成「登入失敗」)。 + exc = PyPtt.TwoFactorAuthRequired() + result = utils._handle_ptt_exception(exc, {}) + assert result["success"] is False + assert result["code"] == "TWO_FACTOR_AUTH_REQUIRED" + assert result["message"] == str(exc) + assert result["message"] != "登入失敗" + + # 回歸保護:LoginError 本身仍要維持 LOGIN_FAILED,不能被上面的分支搶走。 + login_error_result = utils._handle_ptt_exception(PyPtt.LoginError(), {}) + assert login_error_result["code"] == "LOGIN_FAILED" + assert login_error_result["message"] == "登入失敗" + finally: + for m in _SHARED: + mod = snapshot[m] + if mod is None: + sys.modules.pop(m, None) + else: + sys.modules[m] = mod + + if __name__ == "__main__": test_parameter_error_maps_to_parameter_error_code() + test_two_factor_auth_required_maps_to_dedicated_code() print("OK") diff --git a/src/utils.py b/src/utils.py index 964c517..033a870 100644 --- a/src/utils.py +++ b/src/utils.py @@ -29,6 +29,20 @@ def _handle_ptt_exception(e: Exception, kwargs: Dict[str, Any]) -> Dict[str, Any ), } + # TwoFactorAuthRequired 繼承 LoginError,若放進上面的 dict 迴圈會被 + # LoginError 先攔截、蓋成泛用的「登入失敗」。獨立分支、放在迴圈之前, + # 保留 PyPtt 的原始訊息(含如何完成 2FA 驗證的指引)。getattr 是為了 + # 相容裝了舊版 PyPtt(<2.3.6,還沒有這個例外類別)的環境。 + two_factor_auth_required = getattr(PyPtt, "TwoFactorAuthRequired", None) + if two_factor_auth_required is not None and isinstance( + e, two_factor_auth_required + ): + return { + "success": False, + "message": str(e), + "code": "TWO_FACTOR_AUTH_REQUIRED", + } + for exc_type, (message_format, code) in EXCEPTION_MAPPING.items(): if isinstance(e, exc_type): message = (