62 lines
2.1 KiB
Python
62 lines
2.1 KiB
Python
"""唯一允许交给 Android Intent 的商品链接。"""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from urllib.parse import parse_qsl, urlsplit
|
||
|
|
|
||
|
|
|
||
|
|
_SCHEME = "https"
|
||
|
|
_HOST = "mobile.yangkeduo.com"
|
||
|
|
_PATH = "/goods.html"
|
||
|
|
|
||
|
|
|
||
|
|
class ProductUrlError(ValueError):
|
||
|
|
"""输入不是可安全重建的 canonical 商品链接。"""
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class ProductUrl:
|
||
|
|
"""经验证的商品标识及由它重建的 canonical URL。"""
|
||
|
|
|
||
|
|
goods_id: str
|
||
|
|
canonical_url: str
|
||
|
|
|
||
|
|
|
||
|
|
def parse_product_url(value: str) -> ProductUrl:
|
||
|
|
"""只接受一个 ASCII 数字 ``goods_id`` 的拼多多商品直链。
|
||
|
|
|
||
|
|
解析结果绝不原样透传:Intent 使用的 URL 必须从 ``goods_id`` 重新构建,以排除
|
||
|
|
短链、额外参数、userinfo、fragment 和 URL 解析器的边缘表示。
|
||
|
|
"""
|
||
|
|
|
||
|
|
if not isinstance(value, str):
|
||
|
|
raise ProductUrlError("商品链接必须是字符串。")
|
||
|
|
try:
|
||
|
|
parsed = urlsplit(value)
|
||
|
|
port = parsed.port
|
||
|
|
query_pairs = parse_qsl(parsed.query, keep_blank_values=True, strict_parsing=True)
|
||
|
|
except ValueError as error:
|
||
|
|
raise ProductUrlError("商品链接格式无效。") from error
|
||
|
|
|
||
|
|
if (
|
||
|
|
parsed.scheme != _SCHEME
|
||
|
|
or parsed.hostname != _HOST
|
||
|
|
or parsed.username is not None
|
||
|
|
or parsed.password is not None
|
||
|
|
or port is not None
|
||
|
|
or parsed.path != _PATH
|
||
|
|
or parsed.fragment
|
||
|
|
):
|
||
|
|
raise ProductUrlError("商品链接不是允许的拼多多商品直链。")
|
||
|
|
if len(query_pairs) != 1 or query_pairs[0][0] != "goods_id":
|
||
|
|
raise ProductUrlError("商品链接必须且只能包含一个 goods_id 参数。")
|
||
|
|
|
||
|
|
goods_id = query_pairs[0][1]
|
||
|
|
if not goods_id or any(character < "0" or character > "9" for character in goods_id):
|
||
|
|
raise ProductUrlError("goods_id 必须是纯数字。")
|
||
|
|
canonical_url = f"{_SCHEME}://{_HOST}{_PATH}?goods_id={goods_id}"
|
||
|
|
if value != canonical_url:
|
||
|
|
raise ProductUrlError("商品链接必须使用唯一 canonical 表示。")
|
||
|
|
return ProductUrl(goods_id=goods_id, canonical_url=canonical_url)
|