[
  {
    "path": ".github/workflows/reserve.yml",
    "content": "# This is a basic workflow that is manually triggered\n\nname: auto_Reserve\n\n# Controls when the action will run. Workflow runs when manually triggered using the UI\n# or API.\non:\n  schedule:\n    - cron: \"59 21 * * 0,1,2,3,4\" # 周一到周五每天早上5:59启动（防止github action过于高负载）\n  workflow_dispatch:\n  \njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n  \n      - name: Set up Python 3.11\n        uses: actions/setup-python@v2\n        with:\n          python-version: 3.11\n      - name: install dependency\n        run: |\n          python -m pip install --upgrade pip\n          sudo apt-get install build-essential libssl-dev libffi-dev  python3-dev -y\n          pip install cryptography requests opencv-python\n\n      - name: run script\n        env:\n          USERNAMES: ${{ secrets.USERNAMES }}\n          PASSWORDS: ${{ secrets.PASSWORDS }}\n        run: |\n          python main.py -m debug --action\n\n\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\r\n__pycache__/\r\n*.py[cod]\r\n*$py.class\r\n\r\n# C extensions\r\n*.so\r\n\r\n# Distribution / packaging\r\n.Python\r\nbuild/\r\ndevelop-eggs/\r\ndist/\r\ndownloads/\r\neggs/\r\n.eggs/\r\nlib/\r\nlib64/\r\nparts/\r\nsdist/\r\nvar/\r\nwheels/\r\npip-wheel-metadata/\r\nshare/python-wheels/\r\n*.egg-info/\r\n.installed.cfg\r\n*.egg\r\nMANIFEST\r\n\r\n# PyInstaller\r\n#  Usually these files are written by a python script from a template\r\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\r\n*.manifest\r\n*.spec\r\n\r\n# Installer logs\r\npip-log.txt\r\npip-delete-this-directory.txt\r\n\r\n# Unit test / coverage reports\r\nhtmlcov/\r\n.tox/\r\n.nox/\r\n.coverage\r\n.coverage.*\r\n.cache\r\nnosetests.xml\r\ncoverage.xml\r\n*.cover\r\n*.py,cover\r\n.hypothesis/\r\n.pytest_cache/\r\n\r\n# Translations\r\n*.mo\r\n*.pot\r\n\r\n# Django stuff:\r\n*.log\r\nlocal_settings.py\r\ndb.sqlite3\r\ndb.sqlite3-journal\r\n\r\n# Flask stuff:\r\ninstance/\r\n.webassets-cache\r\n\r\n# Scrapy stuff:\r\n.scrapy\r\n\r\n# Sphinx documentation\r\ndocs/_build/\r\n\r\n# PyBuilder\r\ntarget/\r\n\r\n# Jupyter Notebook\r\n.ipynb_checkpoints\r\n\r\n# IPython\r\nprofile_default/\r\nipython_config.py\r\n\r\n# pyenv\r\n.python-version\r\n\r\n# pipenv\r\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\r\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\r\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\r\n#   install all needed dependencies.\r\n#Pipfile.lock\r\n\r\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\r\n__pypackages__/\r\n\r\n# Celery stuff\r\ncelerybeat-schedule\r\ncelerybeat.pid\r\n\r\n# SageMath parsed files\r\n*.sage.py\r\n\r\n# Environments\r\n.env\r\n.venv\r\nenv/\r\nvenv/\r\nENV/\r\nenv.bak/\r\nvenv.bak/\r\n\r\n# Spyder project settings\r\n.spyderproject\r\n.spyproject\r\n\r\n# Rope project settings\r\n.ropeproject\r\n\r\n# mkdocs documentation\r\n/site\r\n\r\n# mypy\r\n.mypy_cache/\r\n.dmypy.json\r\ndmypy.json\r\n\r\n# Pyre type checker\r\n.pyre/\r\ntest.json"
  },
  {
    "path": "README.md",
    "content": "# ChaoXingServerSeat\r\n超星图书馆座位预约脚本\r\n\r\n（由于部分学校新增了点选式行为验证码导致原本的程序会显示验证失败，详细参见issue21[https://github.com/bear-zd/ChaoXingReserveSeat/issues/21]）\r\n\r\n## 注意\r\n\r\n使用python消除了对js的依赖，请拉取最新版程序运行。\r\n\r\n该版本试验性支持滑块验证，目前已经过测试可以使用，如果有滑块验证，请参考下面的**高级设置**部分\r\n\r\n## 如何使用\r\n\r\n### 本地部署方式\r\n\r\n#### 1、安装依赖\r\n\r\n运行脚本前先安装一个包\r\n\r\n```bash\r\npip install cryptography\r\n```\r\n\r\n如果有滑块验证，则需要额外安装numpy和opencv-python\r\n\r\n```bash\r\npip install numpy, opencv-python\r\n```\r\n\r\n#### 2、 获取roomid（图书馆id）和seatid（座位号）\r\n\r\n在使用之前需要先在如下获取图书馆对应的id和座位号，下面的配置里已经提供了上海大学图书馆的id。对于不知道id的，可以通过如下方式进行：\r\n\r\n![image-20231012153826054](https://zideapicbed.oss-cn-shanghai.aliyuncs.com/img/image-20231012153826054.png)\r\n\r\n在进入预约图书馆列表界面时断开网络，点击你想预约的图书馆的`选座`按钮，会提示网页无法打开，此时点击`右上角的三条杠`，选择`复制链接`，会得到类似这样的链接：\r\n\r\n> https://office.chaoxing.com/front/apps/seat/select?id=5483&day=2023-10-12&backLevel=2&pageToken=0f46f3acc7be4c60862cb9815870ddfd\r\n\r\n其中的`id=5483`的5483即为对应图书馆的id，将其填写到config.json中，座位联网后自己挑即可（详细填写参见后面的setting）\r\n\r\n#### 3、running\r\n\r\n由于脚本是检测系统时间为7点时进行预约（在main.py 第16行），如果有特殊要求可以修改。通过 `python main.py` 运行脚本, 添加参数 `-u config.json` 来指明配置文件路径\r\n\r\n运行`python main.py -m debug`可以立即运行查看配置是否正确。\r\n\r\n关于运行的方式，现在提供了多种运行方式：\r\n\r\n- Linux环境下：\r\n\r\n在Linux下可以使用如下方式添加crontab , 运行：`crontab -e`添加指令 :`0 7 * * * python3 main.py`\r\n\r\n- windows环境下：\r\n\r\nwindows下使用时间任务:\r\n\r\n![](https://zideapicbed.oss-cn-shanghai.aliyuncs.com/QQ%E5%9B%BE%E7%89%8720221120213736.png)\r\n\r\n### github actions部署方式（目前应该没有问题了）：\r\n\r\n  这种方式可以不需要在本地部署环境，只需要把fork该仓库并修改配置文件即可。\r\n\r\n1.**fork该仓库**\r\n\r\n2.**修改config.json**：这个仿照之前的方式进行修改即可，但是注意，username和password请留空或者随便填以防止泄漏个人账号密码。（具体的需要填写在自己repo的settings中）。时间什么也是需要修改（修改到仓库中）不要忘记。\r\n\r\n3.**配置账号密码**：在settings->secrets and variables->Repository secrets 创建两个secret keys。名称分别为USERNAMES，PASSWORDS，填写自己的账号和密码即可。（如果有多个用户，请使用,(英文逗号)隔开，如果密码中有逗号可能会出现问题）。\r\n\r\n```\r\nxxxxxxx,xxxxxxx\r\n```\r\n\r\n4.**运行action**：在action -> auto_reserve -> run workflows 选择main分支即可。\r\n\r\n\r\n## config配置\r\n之后编辑config.json并填写座位预约相关信息即可\r\n```json\r\n{\r\n    \"reserve\": [\r\n        {\"username\": \"XXXXXXXX\", //https://passport2.chaoxing.com/mlogin?loginType=1&newversion=true&fid=&  在这个网站查看是否可以顺利登陆 \r\n        \"password\": \"XXXXXXXX\",\r\n        \"time\": [\"08:00\",\"22:00\"], // 预约的起始时间\r\n        \"roomid\":\"2609\", //2609:四楼外圈,5483:四楼内圈,2610:五楼外圈,5484:五楼内圈\r\n        \"seatid\":\"002\", // 注意要用0补全至3位数，例如6号座位应该填006\r\n        \"daysofweek\": [\"Monday\" , \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"]\r\n        },\r\n        {\"username\": \"xxxxxxxxxx\",\r\n        \"password\": \"xxxxxxxxx\",\r\n        \"time\": [\"20:00\",\"21:00\"],\r\n        \"roomid\":\"5483\",\r\n        \"seatid\":[\"056\"],\r\n        \"daysofweek\": [\"Saturday\" , \"Sunday\"]\r\n    }\r\n}\r\n```\r\n参考前面的运行方式即可。\r\n\r\n\r\n## 高级设置\r\n\r\n在main.py中有四个参数可以选择\r\n\r\n```python\r\nSLEEPTIME = 0.2 # 每次抢座的间隔\r\nENDTIME = \"07:01:00\" # 根据学校的开始预约座位时间+1min即可\r\n\r\nENABLE_SLIDER = False # 是否有滑块验证，设置为True开启滑块验证\r\nMAX_ATTEMPT = 4 # 最大尝试次数\r\n```\r\n可以直接进行修改，但是不建议把**SLEEPTIME**设置太小。\r\n\r\n## 存在的问题\r\n\r\n目前日志输出不是很人性化，如果出现了以下问题请提issue：\r\n\r\n- 出现了代码逻辑的错误\r\n- {当前人数过多，请等待5分钟后尝试}。这种是请求方式错误或者请求键值错误导致的，通常是由于学习通更新了预约导致的\r\n- 以字典格式输出的其他错误，仔细查看用户名密码，roomid和seatid是否填写正确。如果问题不能解决请在github上提issue\r\n- 滑块验证目前无法进行测试\r\n\r\n### 无法预约情况debug方式\r\n> 1、电脑端访问：\"https://passport2.chaoxing.com/mlogin?loginType=1&newversion=true&fid=\" 使用自己的用户名密码登录\r\n> 2、电脑端访问：”https://office.chaoxing.com/front/third/apps/seat/code?id={图书馆id}&seatNum={座位id}“查看是否显示时间表\r\n> 3、尝试预约看看是否会出现验证方式\r\n\r\n目前无法实现跨单位座位预约。\r\n\r\n"
  },
  {
    "path": "config.json",
    "content": "{\n    \"reserve\": [\n        {\"username\": \"xxxxxxxxxx\",\n        \"password\": \"xxxxxxxxx\",\n        \"time\": [\"21:00\",\"22:00\"],\n        \"roomid\":\"3993\",\n        \"seatid\":[\"111\"],\n        \"daysofweek\": [\"Monday\" , \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\"]\n    }\n    ]\n}\n"
  },
  {
    "path": "main.py",
    "content": "import json\nimport time\nimport argparse\nimport os\nimport logging\n\nlogging.basicConfig(\n    level=logging.INFO, format=\"%(asctime)s - %(levelname)s - %(message)s\"\n)\n\n\nfrom utils import reserve, get_user_credentials\n\nget_current_time = lambda action: (\n    time.strftime(\"%H:%M:%S\", time.localtime(time.time() + 8 * 3600))\n    if action\n    else time.strftime(\"%H:%M:%S\", time.localtime(time.time()))\n)\nget_current_dayofweek = lambda action: (\n    time.strftime(\"%A\", time.localtime(time.time() + 8 * 3600))\n    if action\n    else time.strftime(\"%A\", time.localtime(time.time()))\n)\n\n\nSLEEPTIME = 0.2  # 每次抢座的间隔\nENDTIME = \"07:01:00\"  # 根据学校的预约座位时间+1min即可\n\nENABLE_SLIDER = True  # 是否有滑块验证\nMAX_ATTEMPT = 5  # 最大尝试次数\nRESERVE_NEXT_DAY = False  # 预约明天而不是今天的\n\n\ndef login_and_reserve(users, usernames, passwords, action, success_list=None):\n    logging.info(\n        f\"Global settings: \\nSLEEPTIME: {SLEEPTIME}\\nENDTIME: {ENDTIME}\\nENABLE_SLIDER: {ENABLE_SLIDER}\\nRESERVE_NEXT_DAY: {RESERVE_NEXT_DAY}\"\n    )\n    if action and len(usernames.split(\",\")) != len(users):\n        raise Exception(\"user number should match the number of config\")\n    if success_list is None:\n        success_list = [False] * len(users)\n    current_dayofweek = get_current_dayofweek(action)\n    for index, user in enumerate(users):\n        username, password, times, roomid, seatid, daysofweek = user.values()\n        if action:\n            username, password = (\n                usernames.split(\",\")[index],\n                passwords.split(\",\")[index],\n            )\n        if current_dayofweek not in daysofweek:\n            logging.info(\"Today not set to reserve\")\n            continue\n        if not success_list[index]:\n            logging.info(\n                f\"----------- {username} -- {times} -- {seatid} try -----------\"\n            )\n            s = reserve(\n                sleep_time=SLEEPTIME,\n                max_attempt=MAX_ATTEMPT,\n                enable_slider=ENABLE_SLIDER,\n                reserve_next_day=RESERVE_NEXT_DAY,\n            )\n            s.get_login_status()\n            s.login(username, password)\n            s.requests.headers.update({\"Host\": \"office.chaoxing.com\"})\n            suc = s.submit(times, roomid, seatid, action)\n            success_list[index] = suc\n    return success_list\n\n\ndef main(users, action=False):\n    current_time = get_current_time(action)\n    logging.info(f\"start time {current_time}, action {'on' if action else 'off'}\")\n    attempt_times = 0\n    usernames, passwords = None, None\n    if action:\n        usernames, passwords = get_user_credentials(action)\n    success_list = None\n    current_dayofweek = get_current_dayofweek(action)\n    today_reservation_num = sum(\n        1 for d in users if current_dayofweek in d.get(\"daysofweek\")\n    )\n    while current_time < ENDTIME:\n        attempt_times += 1\n        # try:\n        success_list = login_and_reserve(\n            users, usernames, passwords, action, success_list\n        )\n        # except Exception as e:\n        #     print(f\"An error occurred: {e}\")\n        print(\n            f\"attempt time {attempt_times}, time now {current_time}, success list {success_list}\"\n        )\n        current_time = get_current_time(action)\n        if sum(success_list) == today_reservation_num:\n            print(f\"reserved successfully!\")\n            return\n\n\ndef debug(users, action=False):\n    logging.info(\n        f\"Global settings: \\nSLEEPTIME: {SLEEPTIME}\\nENDTIME: {ENDTIME}\\nENABLE_SLIDER: {ENABLE_SLIDER}\\nRESERVE_NEXT_DAY: {RESERVE_NEXT_DAY}\"\n    )\n    suc = False\n    logging.info(f\" Debug Mode start! , action {'on' if action else 'off'}\")\n    if action:\n        usernames, passwords = get_user_credentials(action)\n    current_dayofweek = get_current_dayofweek(action)\n    for index, user in enumerate(users):\n        username, password, times, roomid, seatid, daysofweek = user.values()\n        if type(seatid) == str:\n            seatid = [seatid]\n        if action:\n            username, password = (\n                usernames.split(\",\")[index],\n                passwords.split(\",\")[index],\n            )\n        if current_dayofweek not in daysofweek:\n            logging.info(\"Today not set to reserve\")\n            continue\n        logging.info(f\"----------- {username} -- {times} -- {seatid} try -----------\")\n        s = reserve(\n            sleep_time=SLEEPTIME,\n            max_attempt=MAX_ATTEMPT,\n            enable_slider=ENABLE_SLIDER,\n            reserve_next_day=RESERVE_NEXT_DAY,\n        )\n        s.get_login_status()\n        s.login(username, password)\n        s.requests.headers.update({\"Host\": \"office.chaoxing.com\"})\n        suc = s.submit(times, roomid, seatid, action)\n        if suc:\n            return\n\n\ndef get_roomid(args1, args2):\n    username = input(\"请输入用户名：\")\n    password = input(\"请输入密码：\")\n    s = reserve(\n        sleep_time=SLEEPTIME,\n        max_attempt=MAX_ATTEMPT,\n        enable_slider=ENABLE_SLIDER,\n        reserve_next_day=RESERVE_NEXT_DAY,\n    )\n    s.get_login_status()\n    s.login(username=username, password=password)\n    s.requests.headers.update({\"Host\": \"office.chaoxing.com\"})\n    encode = input(\"请输入deptldEnc：\")\n    s.roomid(encode)\n\n\nif __name__ == \"__main__\":\n    config_path = os.path.join(os.path.dirname(__file__), \"config.json\")\n    parser = argparse.ArgumentParser(prog=\"Chao Xing seat auto reserve\")\n    parser.add_argument(\"-u\", \"--user\", default=config_path, help=\"user config file\")\n    parser.add_argument(\n        \"-m\",\n        \"--method\",\n        default=\"reserve\",\n        choices=[\"reserve\", \"debug\", \"room\"],\n        help=\"for debug\",\n    )\n    parser.add_argument(\n        \"-a\",\n        \"--action\",\n        action=\"store_true\",\n        help=\"use --action to enable in github action\",\n    )\n    args = parser.parse_args()\n    func_dict = {\"reserve\": main, \"debug\": debug, \"room\": get_roomid}\n    with open(args.user, \"r+\") as data:\n        usersdata = json.load(data)[\"reserve\"]\n    func_dict[args.method](usersdata, args.action)\n"
  },
  {
    "path": "utils/__init__.py",
    "content": "import os \nfrom .encrypt import AES_Encrypt, generate_captcha_key, enc, verify_param\nfrom .reserve import reserve\n\ndef _fetch_env_variables(env_name, action):\n    try:\n        return os.environ[env_name] if action else \"\"\n    except KeyError:\n        print(f\"Environment variable {env_name} is not configured correctly.\")\n        return None\n\ndef get_user_credentials(action):\n    usernames = _fetch_env_variables('USERNAMES', action)\n    passwords = _fetch_env_variables('PASSWORDS', action)\n    return usernames, passwords"
  },
  {
    "path": "utils/encrypt.py",
    "content": "from cryptography.hazmat.primitives import padding\nfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\nfrom cryptography.hazmat.backends import default_backend\nimport base64\nfrom hashlib import md5\nimport random\nfrom uuid import uuid1\nimport hashlib\n\n\ndef AES_Encrypt(data):\n    key = b\"u2oh6Vu^HWe4_AES\"  # Convert to bytes\n    iv = b\"u2oh6Vu^HWe4_AES\"  # Convert to bytes\n    padder = padding.PKCS7(128).padder()\n    padded_data = padder.update(data.encode(\"utf-8\")) + padder.finalize()\n    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())\n    encryptor = cipher.encryptor()\n    encrypted_data = encryptor.update(padded_data) + encryptor.finalize()\n    enctext = base64.b64encode(encrypted_data).decode(\"utf-8\")\n    return enctext\n\n\ndef resort(submit_info):\n    return {key: submit_info[key] for key in sorted(submit_info.keys())}\n\n\ndef enc(submit_info):\n    add = lambda x, y: x + y\n    processed_info = resort(submit_info)\n    needed = [\n        add(add(\"[\", key), \"=\" + value) + \"]\" for key, value in processed_info.items()\n    ]\n    pattern = \"%sd`~7^/>N4!Q#){''\"\n    needed.append(add(\"[\", pattern) + \"]\")\n    seq = \"\".join(needed)\n    return md5(seq.encode(\"utf-8\")).hexdigest()\n\n\ndef generate_captcha_key(timestamp: int):\n    captcha_key = md5((str(timestamp) + str(uuid1())).encode(\"utf-8\")).hexdigest()\n    encoded_timestamp = (\n        md5(\n            (\n                str(timestamp)\n                + \"42sxgHoTPTKbt0uZxPJ7ssOvtXr3ZgZ1\"\n                + \"slide\"\n                + captcha_key\n            ).encode(\"utf-8\")\n        ).hexdigest()\n        + \":\"\n        + str(int(timestamp) + 0x493E0)\n    )\n    return [captcha_key, encoded_timestamp]\n\n\ndef sort_dict_by_keys(dictionary):\n    \"\"\"将字典按键排序并返回新字典\"\"\"\n    sorted_keys = sorted(dictionary.keys())\n    sorted_dict = {key: dictionary[key] for key in sorted_keys}\n    return sorted_dict\n\n\ndef verify_param(params, algorithm_value):\n    \"\"\"\n    生成参数的MD5验证哈希值\n\n    参数:\n        params: 要验证的参数字典\n        algorithm_value: 对应JavaScript中id为'algorithm'的元素值\n\n    返回:\n        计算得到的MD5哈希字符串\n    \"\"\"\n    # 对参数字典按键排序\n    sorted_params = sort_dict_by_keys(params)\n\n    # 构建哈希字符串列表\n    hash_list = []\n\n    # 遍历排序后的参数，构建格式为 [key=value] 的字符串\n    for key, value in sorted_params.items():\n        # 确保值转换为字符串，与JavaScript行为一致\n        hash_list.append(f\"[{key}={str(value)}]\")\n\n    # 添加algorithm值\n    hash_list.append(f\"[{algorithm_value}]\")\n\n    # 连接所有元素形成最终字符串\n    hash_string = \"\".join(hash_list)\n\n    # 计算MD5哈希值（注意：Python的hashlib返回bytes，需要转换为十六进制字符串）\n    md5_hash = hashlib.md5(hash_string.encode(\"utf-8\")).hexdigest()\n\n    return md5_hash\n"
  },
  {
    "path": "utils/reserve.py",
    "content": "from utils import AES_Encrypt, enc, generate_captcha_key, verify_param\nimport json\nimport requests\nimport re\nimport time\nimport logging\nimport datetime\nfrom urllib3.exceptions import InsecureRequestWarning\n\n\ndef get_date(day_offset: int = 0):\n    today = datetime.datetime.now().date()\n    offset_day = today + datetime.timedelta(days=day_offset)\n    tomorrow = offset_day.strftime(\"%Y-%m-%d\")\n    return tomorrow\n\n\nclass reserve:\n    def __init__(\n        self,\n        sleep_time=0.2,\n        max_attempt=50,\n        enable_slider=False,\n        reserve_next_day=False,\n    ):\n        self.login_page = (\n            \"https://passport2.chaoxing.com/mlogin?loginType=1&newversion=true&fid=\"\n        )\n        self.url = (\n            \"https://office.chaoxing.com/front/third/apps/seat/code?id={}&seatNum={}\"\n        )\n        self.submit_url = \"https://office.chaoxing.com/data/apps/seat/submit\"\n        self.seat_url = \"https://office.chaoxing.com/data/apps/seat/getusedtimes\"\n        self.login_url = \"https://passport2.chaoxing.com/fanyalogin\"\n        self.token = \"\"\n        self.success_times = 0\n        self.fail_dict = []\n        self.submit_msg = []\n        self.requests = requests.session()\n        self.token_pattern = re.compile(\"token = '(.*?)'\")\n        self.headers = {\n            \"Referer\": \"https://office.chaoxing.com/\",\n            \"Host\": \"captcha.chaoxing.com\",\n            \"Pragma\": \"no-cache\",\n            \"Sec-Ch-Ua\": '\"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"',\n            \"Sec-Ch-Ua-Mobile\": \"?0\",\n            \"Sec-Ch-Ua-Platform\": '\"Linux\"',\n            \"Sec-Fetch-Dest\": \"document\",\n            \"Sec-Fetch-Mode\": \"navigate\",\n            \"Sec-Fetch-Site\": \"none\",\n            \"Sec-Fetch-User\": \"?1\",\n            \"Upgrade-Insecure-Requests\": \"1\",\n            \"User-Agent\": \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36\",\n        }\n        self.login_headers = {\n            \"Accept\": \"application/json, text/javascript, */*; q=0.01\",\n            \"accept-encoding\": \"gzip, deflate, br, zstd\",\n            \"cache-control\": \"no-cache\",\n            \"Connection\": \"keep-alive\",\n            \"Accept-Language\": \"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7\",\n            \"User-Agent\": \"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.3 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1 wechatdevtools/1.05.2109131 MicroMessenger/8.0.5 Language/zh_CN webview/16364215743155638\",\n            \"X-Requested-With\": \"XMLHttpRequest\",\n            \"Content-Type\": \"application/x-www-form-urlencoded; charset=UTF-8\",\n            \"Host\": \"passport2.chaoxing.com\",\n        }\n\n        self.sleep_time = sleep_time\n        self.max_attempt = max_attempt\n        self.enable_slider = enable_slider\n        self.reserve_next_day = reserve_next_day\n        requests.packages.urllib3.disable_warnings(InsecureRequestWarning)\n\n    # login and page token\n    def _get_page_token(self, url, require_value=False):\n        response = self.requests.get(url=url, verify=False)\n        html = response.content.decode(\"utf-8\")\n        # matches = re.findall(r\"token = \\'(.*?)\\'\", html)\n        matches = re.findall(r'id=\"submit_enc\"\\s+value=\"(.*?)\"', html)\n        value_matches = None\n        if require_value:\n            value_matches = re.findall(r'value=\"(.*?)\"', html)\n            if not matches:\n                logging.error(f\"Failed to get token from {url}\")\n                return \"\", \"\"\n            if not value_matches:\n                logging.error(f\"Failed to get submit value from {url}\")\n                return matches[0], \"\"\n        return matches[0] if matches else \"\", value_matches[0] if value_matches else \"\"\n\n    def get_login_status(self):\n        self.requests.headers = self.login_headers\n        self.requests.get(url=self.login_page, verify=False)\n\n    def login(self, username, password):\n        username = AES_Encrypt(username)\n        password = AES_Encrypt(password)\n        parm = {\n            \"fid\": -1,\n            \"uname\": username,\n            \"password\": password,\n            \"refer\": \"http%3A%2F%2Foffice.chaoxing.com%2Ffront%2Fthird%2Fapps%2Fseat%2Fcode%3Fid%3D4219%26seatNum%3D380\",\n            \"t\": True,\n        }\n        jsons = self.requests.post(url=self.login_url, params=parm, verify=False)\n        obj = jsons.json()\n        if obj[\"status\"]:\n            logging.info(f\"User {username} login successfully\")\n            return (True, \"\")\n        else:\n            logging.info(\n                f\"User {username} login failed. Please check you password and username! \"\n            )\n            return (False, obj[\"msg2\"])\n\n    # extra: get roomid\n    def roomid(self, encode):\n        url = f\"https://office.chaoxing.com/data/apps/seat/room/list?cpage=1&pageSize=100&firstLevelName=&secondLevelName=&thirdLevelName=&deptIdEnc={encode}\"\n        json_data = self.requests.get(url=url).content.decode(\"utf-8\")\n        ori_data = json.loads(json_data)\n        for i in ori_data[\"data\"][\"seatRoomList\"]:\n            info = f'{i[\"firstLevelName\"]}-{i[\"secondLevelName\"]}-{i[\"thirdLevelName\"]} id为：{i[\"id\"]}'\n            print(info)\n\n    # solve captcha\n\n    def resolve_captcha(self):\n        logging.info(f\"Start to resolve captcha token\")\n        captcha_token, bg, tp = self.get_slide_captcha_data()\n        logging.info(f\"Successfully get prepared captcha_token {captcha_token}\")\n        logging.info(f\"Captcha Image URL-small {tp}, URL-big {bg}\")\n        x = self.x_distance(bg, tp)\n        logging.info(f\"Successfully calculate the captcha distance {x}\")\n\n        params = {\n            \"callback\": \"jQuery33109180509737430778_1716381333117\",\n            \"captchaId\": \"42sxgHoTPTKbt0uZxPJ7ssOvtXr3ZgZ1\",\n            \"type\": \"slide\",\n            \"token\": captcha_token,\n            \"textClickArr\": json.dumps([{\"x\": x}]),\n            \"coordinate\": json.dumps([]),\n            \"runEnv\": \"10\",\n            \"version\": \"1.1.18\",\n            \"_\": int(time.time() * 1000),\n        }\n        response = self.requests.get(\n            f\"https://captcha.chaoxing.com/captcha/check/verification/result\",\n            params=params,\n            headers=self.headers,\n        )\n        text = response.text.replace(\n            \"jQuery33109180509737430778_1716381333117(\", \"\"\n        ).replace(\")\", \"\")\n        data = json.loads(text)\n        logging.info(f\"Successfully resolve the captcha token {data}\")\n        try:\n            validate_val = json.loads(data[\"extraData\"])[\"validate\"]\n            return validate_val\n        except KeyError as e:\n            logging.info(\"Can't load validate value. Maybe server return mistake.\")\n            return \"\"\n\n    def get_slide_captcha_data(self):\n        url = \"https://captcha.chaoxing.com/captcha/get/verification/image\"\n        timestamp = int(time.time() * 1000)\n        capture_key, token = generate_captcha_key(timestamp)\n        referer = f\"https://office.chaoxing.com/front/third/apps/seat/code?id=3993&seatNum=0199\"\n        params = {\n            \"callback\": f\"jQuery33107685004390294206_1716461324846\",\n            \"captchaId\": \"42sxgHoTPTKbt0uZxPJ7ssOvtXr3ZgZ1\",\n            \"type\": \"slide\",\n            \"version\": \"1.1.18\",\n            \"captchaKey\": capture_key,\n            \"token\": token,\n            \"referer\": referer,\n            \"_\": timestamp,\n            \"d\": \"a\",\n            \"b\": \"a\",\n        }\n        response = self.requests.get(url=url, params=params, headers=self.headers)\n        content = response.text\n\n        data = content.replace(\n            \"jQuery33107685004390294206_1716461324846(\", \")\"\n        ).replace(\")\", \"\")\n        data = json.loads(data)\n        captcha_token = data[\"token\"]\n        bg = data[\"imageVerificationVo\"][\"shadeImage\"]\n        tp = data[\"imageVerificationVo\"][\"cutoutImage\"]\n        return captcha_token, bg, tp\n\n    def x_distance(self, bg, tp):\n        import numpy as np\n        import cv2\n\n        def cut_slide(slide):\n            slider_array = np.frombuffer(slide, np.uint8)\n            slider_image = cv2.imdecode(slider_array, cv2.IMREAD_UNCHANGED)\n            slider_part = slider_image[:, :, :3]\n            mask = slider_image[:, :, 3]\n            mask[mask != 0] = 255\n            x, y, w, h = cv2.boundingRect(mask)\n            cropped_image = slider_part[y : y + h, x : x + w]\n            return cropped_image\n\n        c_captcha_headers = {\n            \"Referer\": \"https://office.chaoxing.com/\",\n            \"Host\": \"captcha-b.chaoxing.com\",\n            \"Pragma\": \"no-cache\",\n            \"Sec-Ch-Ua\": '\"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"',\n            \"Sec-Ch-Ua-Mobile\": \"?0\",\n            \"Sec-Ch-Ua-Platform\": '\"Linux\"',\n            \"Sec-Fetch-Dest\": \"document\",\n            \"Sec-Fetch-Mode\": \"navigate\",\n            \"Sec-Fetch-Site\": \"none\",\n            \"Sec-Fetch-User\": \"?1\",\n            \"Upgrade-Insecure-Requests\": \"1\",\n            \"User-Agent\": \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36\",\n        }\n        bgc, tpc = self.requests.get(bg, headers=c_captcha_headers), self.requests.get(\n            tp, headers=c_captcha_headers\n        )\n        bg, tp = bgc.content, tpc.content\n        bg_img = cv2.imdecode(np.frombuffer(bg, np.uint8), cv2.IMREAD_COLOR)\n        tp_img = cut_slide(tp)\n        bg_edge = cv2.Canny(bg_img, 100, 200)\n        tp_edge = cv2.Canny(tp_img, 100, 200)\n        bg_pic = cv2.cvtColor(bg_edge, cv2.COLOR_GRAY2RGB)\n        tp_pic = cv2.cvtColor(tp_edge, cv2.COLOR_GRAY2RGB)\n        res = cv2.matchTemplate(bg_pic, tp_pic, cv2.TM_CCOEFF_NORMED)\n        _, _, _, max_loc = cv2.minMaxLoc(res)\n        tl = max_loc\n        return tl[0]\n\n    def submit(self, times, roomid, seatid, action):\n        for seat in seatid:\n            suc = False\n            while ~suc and self.max_attempt > 0:\n                token, value = self._get_page_token(\n                    self.url.format(roomid, seat), require_value=True\n                )\n                logging.info(f\"Get token: {token}\")\n                captcha = self.resolve_captcha() if self.enable_slider else \"\"\n                logging.info(f\"Captcha token {captcha}\")\n                suc = self.get_submit(\n                    self.submit_url,\n                    times=times,\n                    token=token,\n                    roomid=roomid,\n                    seatid=seat,\n                    captcha=captcha,\n                    action=action,\n                    value=value,\n                )\n                if suc:\n                    return suc\n                time.sleep(self.sleep_time)\n                self.max_attempt -= 1\n        return suc\n\n    def get_submit(\n        self, url, times, token, roomid, seatid, captcha=\"\", action=False, value=\"\"\n    ):\n        delta_day = 1 if self.reserve_next_day else 0\n        day = datetime.date.today() + datetime.timedelta(\n            days=0 + delta_day\n        )  # 预约今天，修改days=1表示预约明天\n        if action:\n            day = datetime.date.today() + datetime.timedelta(\n                days=1 + delta_day\n            )  # 由于action时区问题导致其早+8区一天\n        parm = {\n            \"roomId\": roomid,\n            \"startTime\": times[0],\n            \"endTime\": times[1],\n            \"day\": str(day),\n            \"seatNum\": seatid,\n            \"captcha\": captcha,\n            \"token\": token,\n            \"type\": \"1\",\n            \"verifyData\": \"1\",\n        }\n        logging.info(f\"submit parameter {parm} \")\n        # parm[\"enc\"] = enc(parm)\n        parm[\"enc\"] = verify_param(parm, value)\n        html = self.requests.post(url=url, params=parm, verify=True).content.decode(\n            \"utf-8\"\n        )\n        self.submit_msg.append(\n            times[0] + \"~\" + times[1] + \":  \" + str(json.loads(html))\n        )\n        logging.info(json.loads(html))\n        return json.loads(html)[\"success\"]\n"
  }
]