├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── checkin.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── app ├── .dockerignore ├── Dockerfile ├── config │ └── config_example.toml ├── main.py ├── notify │ └── notify.py ├── requirements.txt ├── scheduler.py └── utils │ ├── file_helper.py │ ├── smzdm_bot.py │ └── smzdm_tasks.py ├── docker-compose.yml └── smzdm_ql.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build image 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - "main" 7 | 8 | jobs: 9 | buildx: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | submodules: true 16 | 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v2 19 | 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v2 22 | 23 | - name: Cache Docker layers 24 | uses: actions/cache@v3 25 | with: 26 | path: /tmp/.buildx-cache 27 | key: ${{ runner.os }}-buildx-${{ github.sha }} 28 | restore-keys: | 29 | ${{ runner.os }}-buildx- 30 | 31 | - name: Login to DockerHub 32 | uses: docker/login-action@v2 33 | with: 34 | username: ${{ secrets.DOCKERHUB_USERNAME }} 35 | password: ${{ secrets.DOCKERHUB_TOKEN }} 36 | 37 | - name: Lower case 38 | id: string 39 | uses: ASzc/change-string-case-action@v5 40 | with: 41 | string: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} 42 | 43 | - name: Build and push 44 | uses: docker/build-push-action@v4 45 | with: 46 | context: app 47 | platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386 48 | push: true 49 | tags: ${{ steps.string.outputs.lowercase }}:latest 50 | cache-from: type=local,src=/tmp/.buildx-cache 51 | cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max 52 | 53 | - name: Sync README.md 54 | uses: ms-jpq/sync-dockerhub-readme@v1 55 | with: 56 | username: ${{ secrets.DOCKERHUB_USERNAME }} 57 | password: ${{ secrets.DOCKERHUB_TOKEN }} 58 | repository: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} 59 | readme: "./README.md" 60 | 61 | - name: Move cache 62 | run: | 63 | rm -rf /tmp/.buildx-cache 64 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 65 | -------------------------------------------------------------------------------- /.github/workflows/checkin.yml: -------------------------------------------------------------------------------- 1 | name: Check in 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_run: 6 | workflows: ["Build image"] 7 | types: 8 | - completed 9 | 10 | # UTC时间,对应Beijing时间 8:30 11 | # schedule: 12 | # - cron: "30 0 * * *" 13 | 14 | jobs: 15 | run: 16 | runs-on: ubuntu-latest 17 | container: 18 | image: enwaiax/smzdm_bot 19 | env: 20 | ANDROID_COOKIE: ${{ secrets.ANDROID_COOKIE }} 21 | SK: ${{ secrets.SK }} 22 | PUSH_PLUS_TOKEN: ${{ secrets.PUSH_PLUS_TOKEN }} 23 | SC_KEY: ${{ secrets.SC_KEY }} 24 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }} 25 | TG_USER_ID: ${{ secrets.TG_USER_ID }} 26 | TG_BOT_API: ${{ secrets.TG_BOT_API }} 27 | steps: 28 | - name: Working 29 | run: | 30 | python /smzdm_bot/main.py 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # cookies 132 | cookies.json 133 | config.toml 134 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Python: Current File", 6 | "type": "python", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/app/main.py", 9 | "console": "integratedTerminal", 10 | "justMyCode": true, 11 | "envFile": "${workspaceFolder}/.env" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 什么值得买每日签到脚本 2 | 3 |

4 | 5 | 6 | 7 | 8 |

9 | 10 | ## 更新日志 11 | 12 | - 2022-12-08, 签到失败,浏览器端签到需要滑动验证码认证 13 | - 2023-01-11, 更改`User-Agent`为`iPhone`后可`bypass`滑块认证 14 | - 2023-01-14, 登录认证失败, 签到失效 15 | - 2023-02-18, 通过安卓端验证登录,感谢 [jzksnsjswkw/smzdm-app](https://github.com/jzksnsjswkw/smzdm-app) 的思路. 旧版代码查看 [old](https://github.com/Chasing66/smzdm_bot/tree/old) 分支 16 | - 2023-02-25, 新增`all_reward` 和`extra_reward`两个接口,本地支持多用户运行 17 | - 2023-02-27, 修复本地 docker-compose 运行问题; 本地 docker-compose 支持多账号运行 18 | - 2023-03-01, 支持青龙面板且支持多账号 19 | - 2023-03-01, 仅需要`ANDROID_COOKIE`和`SK`两个变量,自动生成`USER_AGENT`和`TOKEN`, 引入随机休眠,减小被封概率 20 | - 2023-03-02, 新增每日抽奖,参考 hex-ci 的[思路](https://github.com/hex-ci/smzdm_script/blob/main/smzdm_lottery.js) 21 | - 2023-04-06, 新增企业微信BOT-WEBHOOK通知推送方式,仅需要`ANDROID_COOKIE`一个变量, `SK`改为可选变量. 如果能够通过抓包抓到,最好填上. 22 | - 2023-04-23,更新抽奖功能 23 | 24 | ## 1. 实现功能 25 | 26 | - 每日签到, 额外奖励,随机奖励 27 | - 多种运行方式: GitHub Action, 本地运行,docker, 青龙面板 28 | - 多种通知方式: `pushplus`, `server酱`,`企业微信bot-webhook`, `telegram bot`(支持自定义反代`Telegram Bot API`. [搭建教程](https://anerg.com/2022/07/25/reverse-proxy-telegram-bot-api-using-cloudflare-worker.html)) 29 | - 支持多账号(需配置`config.toml`) 30 | 31 | ## 2. 配置 32 | 33 | 支持两种读取配置的方法,从`环境变量`或者`config.toml`中读取 34 | 35 | ### 2.1 从环境变量中读取配置 36 | 37 | ```conf 38 | # Cookie 39 | ANDROID_COOKIE = "" 40 | SK = "" # 可选,如果抓包抓到最好设置 41 | 42 | # Notification 43 | PUSH_PLUS_TOKEN = "" 44 | SC_KEY = "" 45 | WECOM_BOT_WEBHOOK = "" 46 | TG_BOT_TOKEN = "" 47 | TG_USER_ID = "" 48 | 49 | # 用于自定义反代的Telegram Bot API(按需设置) 50 | TG_BOT_API = "" 51 | 52 | # 用于docker运行的定时设定(可选),未设定则随机定时执行 53 | SCH_HOUR= 54 | SCH_MINUTE= 55 | ``` 56 | 57 | ### 2.2 从`config.toml`中读取 58 | 59 | 参考模板 [app/config/config_example.toml](https://github.com/Chasing66/smzdm_bot/blob/main/app/config/config_example.toml) 60 | 61 | ```toml 62 | [user.A] 63 | ANDROID_COOKIE = "" 64 | SK = "" # 可选,如果抓包抓到最好设置 65 | 66 | [user.B] 67 | # Disable userB的签到. 不配置此参数默认启用该用户 68 | Disable = true 69 | ANDROID_COOKIE = "" 70 | SK = "" # 可选,如果抓包抓到最好设置 71 | 72 | [notify] 73 | PUSH_PLUS_TOKEN = "" 74 | SC_KEY = "" 75 | WECOM_BOT_WEBHOOK = "" 76 | TG_BOT_TOKEN = "" 77 | TG_USER_ID = "" 78 | TG_BOT_API = "" 79 | ``` 80 | 81 | ## 3. 使用 82 | 83 | ### 3.1 青龙面板 84 | 85 | ``` 86 | ql repo https://github.com/Chasing66/smzdm_bot.git "smzdm_ql.py" 87 | ``` 88 | 89 | 默认情况下从环境变量读取配置,仅支持单用户. 90 | 91 | 如果需要支持多用户,推荐使用`config.toml`, 配置参考 [2.2 从`config.toml`中读取](#22-从configtoml中读取). 92 | 配置完成后, 拷贝`config.toml`到青龙容器内的`/ql/data/repo/Chasing66_smzdm_bot/app/config` 93 | 94 | ``` 95 | docker cp config.toml <你的青龙容器名称>:/ql/data/repo/Chasing66_smzdm_bot/app/config 96 | ``` 97 | 98 | ### 3.2 本地直接运行 99 | 100 | 克隆本项目到本地, 按照需求配置,配置参考 [2.2 从`config.toml`中读取](#22-从configtoml中读取) 101 | 102 | ```bash 103 | python3 -m venv .venv 104 | source .venv/bin/activate 105 | cd app 106 | pip install -r requirements.txt 107 | python main.py 108 | ``` 109 | 110 | ### 3.3 本地 docker-compose 运行 111 | 112 | 配置参考[2.2 从`config.toml`中读取](#22-从configtoml中读取) 113 | 114 | 修改 [docker-compose.yaml](https://github.com/Chasing66/smzdm_bot/blob/main/docker-compose.yml), 将`app/config/config.toml`mout 到容器内`/smzdm_bot/config/config.toml` 115 | 116 | ```yaml 117 | version: "3.9" 118 | services: 119 | smzdm_bot: 120 | image: enwaiax/smzdm_bot 121 | container_name: smzdm_bot 122 | restart: on-failure 123 | logging: 124 | driver: "json-file" 125 | options: 126 | max-size: "1m" 127 | max-file: "1" 128 | volumes: 129 | - ./app/config/config.toml:/smzdm_bot/config/config.toml 130 | ``` 131 | 132 | ### 3.4 Git Action 运行 133 | 134 | > GitHub Action 禁止对于 Action 资源的滥用,请尽可能使用其他方式 135 | 136 | GitHub Action 仅支持`env`配置方式, **务必自行更改为随机时间** 137 | 138 | 1. Fork[此仓库项目](https://github.com/Chasing66/smzdm_bot)>, 欢迎`star`~ 139 | 2. 修改 `.github/workflows/checkin.yml`里的下面部分, 取消`schedule`两行的注释,自行设定时间 140 | 141 | ```yaml 142 | # UTC时间,对应Beijing时间 9:30 143 | schedule: 144 | - cron: "30 1 * * *" 145 | ``` 146 | 147 | 3. 配置参考 [2.1.1 从环境变量中读取配置](#21-从环境变量中读取配置) 148 | 149 | ## 4. 其它 150 | 151 | ### 4.1 手机抓包 152 | 153 | > 抓包有一定门槛,请自行尝试! 如果实在解决不了,请我喝瓶可乐可以帮忙 154 | 155 | 抓包工具可使用 HttpCanary,教程参考[HttpCanary 抓包](https://juejin.cn/post/7177682063699968061) 156 | 157 | 1. 按照上述教程配置好 HttpCanary 158 | 2. 开始抓包,并打开什么值得买 APP 159 | 3. 过滤`https://user-api.smzdm.com/checkin`的`post`请求并查看 160 | 4. 点击右上角分享,分享 cURL,复制保存该命令 161 | 5. 将复制的 curl 命令转换为 python 格式,[方法](https://curlconverter.com/) 162 | 6. 填入转换后的`Cookies`和`sk`. `Cookies`在`headers`里,`sk`在`data`里, `sk`是可选项 163 | 164 | ## 5. Stargazers over time 165 | 166 | [![Stargazers over time](https://starchart.cc/Chasing66/smzdm_bot.svg)](https://starchart.cc/Chasing66/smzdm_bot) 167 | -------------------------------------------------------------------------------- /app/.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .github/ 3 | .venv/ 4 | .vscode/ 5 | config/config.toml 6 | config/cookies.json 7 | .env 8 | .dockerignore 9 | .gitignore 10 | Dockerfile 11 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine as builder 2 | 3 | RUN apk update && apk add --no-cache tzdata ca-certificates 4 | ADD requirements.txt /tmp/ 5 | RUN pip3 install --user -r /tmp/requirements.txt 6 | 7 | 8 | FROM python:alpine 9 | WORKDIR /smzdm_bot 10 | ENV TZ=Asia/Shanghai 11 | 12 | COPY --from=builder /root/.local /usr/local 13 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 14 | COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo 15 | COPY . /smzdm_bot 16 | 17 | CMD [ "python", "scheduler.py" ] 18 | -------------------------------------------------------------------------------- /app/config/config_example.toml: -------------------------------------------------------------------------------- 1 | [user.A] 2 | ANDROID_COOKIE = "" 3 | 4 | [user.B] 5 | ANDROID_COOKIE = "" 6 | 7 | [notify] 8 | PUSH_PLUS_TOKEN = "" 9 | SC_KEY = "" 10 | TG_BOT_TOKEN = "" 11 | TG_USER_ID = "" 12 | TG_BOT_API = "" 13 | WECOM_BOT_WEBHOOK = "" 14 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | 5 | from loguru import logger 6 | from notify.notify import NotifyBot 7 | from utils.file_helper import TomlHelper 8 | from utils.smzdm_bot import SmzdmBot 9 | from utils.smzdm_tasks import SmzdmTasks 10 | 11 | CURRENT_PATH = Path(__file__).parent.resolve() 12 | CONFIG_FILE = Path(CURRENT_PATH, "config/config.toml") 13 | 14 | logger.add("smzdm.log", retention="10 days") 15 | 16 | 17 | def load_conf(): 18 | conf_kwargs = {} 19 | 20 | if Path.exists(CONFIG_FILE): 21 | logger.info("Get configration from config.toml") 22 | conf_kwargs = TomlHelper(CONFIG_FILE).read() 23 | conf_kwargs.update({"toml_conf": True}) 24 | elif os.environ.get("ANDROID_COOKIE", None): 25 | logger.info("Get configration from env") 26 | conf_kwargs = { 27 | "SK": os.environ.get("SK"), 28 | "ANDROID_COOKIE": os.environ.get("ANDROID_COOKIE"), 29 | "PUSH_PLUS_TOKEN": os.environ.get("PUSH_PLUS_TOKEN", None), 30 | "SC_KEY": os.environ.get("SC_KEY", None), 31 | "TG_BOT_TOKEN": os.environ.get("TG_BOT_TOKEN", None), 32 | "TG_USER_ID": os.environ.get("TG_USER_ID", None), 33 | "TG_BOT_API": os.environ.get("TG_BOT_API", None), 34 | } 35 | conf_kwargs.update({"env_conf": True}) 36 | else: 37 | logger.info("Please set cookies first") 38 | sys.exit(1) 39 | return conf_kwargs 40 | 41 | 42 | def main(): 43 | conf_kwargs = load_conf() 44 | msg = "" 45 | if conf_kwargs.get("toml_conf"): 46 | for user, config in conf_kwargs["user"].items(): 47 | if config.get("Disable"): 48 | logger.info(f"===== Skip task for user: {user} =====") 49 | continue 50 | logger.info((f"===== Start task for user: {user} =====")) 51 | try: 52 | bot = SmzdmBot(**config) 53 | tasks = SmzdmTasks(bot) 54 | msg += tasks.checkin() 55 | msg += tasks.vip_info() 56 | msg += tasks.all_reward() 57 | tasks.extra_reward() 58 | msg += tasks.lottery() 59 | except Exception as e: 60 | logger.error(e) 61 | continue 62 | if not msg: 63 | logger.error("No msg generated") 64 | return 65 | NotifyBot(content=msg, **conf_kwargs["notify"]) 66 | else: 67 | bot = SmzdmBot(**conf_kwargs) 68 | tasks = SmzdmTasks(bot) 69 | msg += tasks.checkin() 70 | msg += tasks.vip_info() 71 | msg += tasks.all_reward() 72 | tasks.extra_reward() 73 | msg += tasks.lottery() 74 | NotifyBot(content=msg, **conf_kwargs) 75 | if msg is None or "Fail to login in" in msg: 76 | logger.error("Fail the Github action job") 77 | sys.exit(1) 78 | 79 | 80 | if __name__ == "__main__": 81 | main() 82 | -------------------------------------------------------------------------------- /app/notify/notify.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Dict 3 | from urllib.parse import urljoin 4 | 5 | import requests 6 | from loguru import logger 7 | 8 | 9 | class NotifyBot(object): 10 | def __init__(self, content, title="什么值得买签到", **kwargs: Dict) -> None: 11 | self.content = content 12 | self.title = title 13 | self.kwargs = kwargs 14 | 15 | self.push_plus() 16 | self.server_chain() 17 | self.wecom() 18 | self.tg_bot() 19 | 20 | def push_plus(self, template="html"): 21 | if not self.kwargs.get("PUSH_PLUS_TOKEN", None): 22 | logger.warning("⚠️ PUSH_PLUS_TOKEN not set, skip PushPlus nofitication") 23 | return 24 | PUSH_PLUS_TOKEN = self.kwargs.get("PUSH_PLUS_TOKEN") 25 | 26 | url = "https://www.pushplus.plus/send" 27 | body = { 28 | "token": PUSH_PLUS_TOKEN, 29 | "title": self.title, 30 | "content": self.content, 31 | "template": template, 32 | } 33 | data = json.dumps(body).encode(encoding="utf-8") 34 | headers = {"Content-Type": "application/json"} 35 | try: 36 | resp = requests.post(url, data=data, headers=headers) 37 | if resp.ok: 38 | logger.info("✅ Push Plus notified") 39 | else: 40 | logger.warning("Fail to notify Push Plus") 41 | except Exception as e: 42 | logger.error(e) 43 | 44 | def server_chain(self): 45 | if not self.kwargs.get("SC_KEY", None): 46 | logger.warning("⚠️ SC_KEY not set, skip ServerChain notification") 47 | return 48 | SC_KEY = self.kwargs.get("SC_KEY") 49 | url = f"http://sc.ftqq.com/{SC_KEY}.send" 50 | data = {"text": self.title, "desp": self.content} 51 | try: 52 | resp = requests.post(url, data=data) 53 | if resp.ok: 54 | logger.info("✅ Server Chain notified") 55 | else: 56 | logger.warning("Fail to notify Server Chain") 57 | except Exception as e: 58 | logger.error(e) 59 | 60 | def wecom(self): 61 | if not self.kwargs.get("WECOM_BOT_WEBHOOK", None): 62 | logger.warning("⚠️ WECOM_BOT_WEBHOOK not set, skip WeCom notification") 63 | return 64 | WECOM_BOT_WEBHOOK = self.kwargs.get("WECOM_BOT_WEBHOOK") 65 | message = { 66 | "msgtype": "text", 67 | "text": {"content": f"{self.title}\n{self.content}"}, 68 | } 69 | try: 70 | resp = requests.post(WECOM_BOT_WEBHOOK, data=json.dumps(message)) 71 | if resp.ok: 72 | logger.info("✅ WeCom notified") 73 | else: 74 | logger.warning("Fail to notify WeCom") 75 | except Exception as e: 76 | logger.error(e) 77 | 78 | def tg_bot(self): 79 | if not self.kwargs.get("TG_BOT_TOKEN", None) or not self.kwargs.get( 80 | "TG_USER_ID", None 81 | ): 82 | logger.warning("⚠️ Skip TelegramBot notification") 83 | return 84 | TG_BOT_TOKEN = self.kwargs.get("TG_BOT_TOKEN") 85 | TG_USER_ID = self.kwargs.get("TG_USER_ID") 86 | if self.kwargs.get("TG_BOT_API"): 87 | url = urljoin( 88 | self.kwargs.get("TG_BOT_API"), f"/bot{TG_BOT_TOKEN}/sendMessage" 89 | ) 90 | else: 91 | url = f"https://api.telegram.org/bot{TG_BOT_TOKEN}/sendMessage" 92 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 93 | params = { 94 | "chat_id": str(TG_USER_ID), 95 | "text": f"{self.title}\n{self.content}", 96 | "disable_web_page_preview": "true", 97 | } 98 | try: 99 | resp = requests.post(url=url, headers=headers, params=params) 100 | if resp.ok: 101 | logger.info("✅ Telegram Bot notified") 102 | else: 103 | logger.warning("Fail to notify TelegramBot") 104 | except Exception as e: 105 | logger.error(e) 106 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | apscheduler>=3.10.1 2 | loguru>=0.7.0 3 | prettytable>=3.4.1 4 | requests>=2.28.0 5 | toml>=0.10.2 6 | -------------------------------------------------------------------------------- /app/scheduler.py: -------------------------------------------------------------------------------- 1 | import os 2 | from random import randint 3 | 4 | from apscheduler.schedulers.background import BlockingScheduler 5 | from loguru import logger 6 | from main import main 7 | 8 | if __name__ == "__main__": 9 | logger.info("First time run") 10 | main() 11 | SCH_HOUR = randint(0, 23) if not os.environ.get("SCH_HOUR") else os.environ.get("SCH_HOUR") 12 | SCH_MINUTE = randint(0, 59) if not os.environ.get("SCH_HOUR") else os.environ.get("SCH_HOUR") 13 | logger.info(f"The scheduelr time is: {SCH_HOUR}:{SCH_MINUTE}") 14 | scheduler = BlockingScheduler(timezone="Asia/Shanghai") 15 | scheduler.add_job(main, "cron", hour=SCH_HOUR, minute=SCH_MINUTE) 16 | print("Press Ctrl+{0} to exit".format("Break" if os.name == "nt" else "C")) 17 | try: 18 | scheduler.start() 19 | except (KeyboardInterrupt, SystemExit): 20 | pass 21 | -------------------------------------------------------------------------------- /app/utils/file_helper.py: -------------------------------------------------------------------------------- 1 | import toml 2 | 3 | 4 | class TomlHelper: 5 | def __init__(self, toml_filename): 6 | self.t_dict = dict() 7 | self.toml_file_path = toml_filename 8 | 9 | def update(self, t_data): 10 | self.t_dict.update(t_data) 11 | return self.t_dict 12 | 13 | def write(self, t_data): 14 | with open(self.toml_file_path, "w", encoding="utf-8") as fs: 15 | toml.dump(t_data, fs) 16 | 17 | def read(self): 18 | with open(self.toml_file_path, "r", encoding="utf-8") as fs: 19 | t_data = toml.load(fs) 20 | return t_data 21 | 22 | def read_str(self, s_data): 23 | t_data = toml.loads(s_data, _dict=dict) 24 | return t_data 25 | 26 | def read_dict(self, dict): 27 | t_data = toml.dumps(dict) 28 | return t_data 29 | -------------------------------------------------------------------------------- /app/utils/smzdm_bot.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import re 3 | import time 4 | from random import randint 5 | from urllib.parse import unquote 6 | 7 | import requests 8 | 9 | 10 | class SmzdmBot: 11 | SIGN_KEY = "apr1$AwP!wRRT$gJ/q.X24poeBInlUJC" 12 | 13 | def __init__(self, ANDROID_COOKIE: str, SK=None, **kwargs): 14 | self.cookies = unquote(ANDROID_COOKIE) 15 | self.sk = SK 16 | self.cookies_dict = self._cookies_to_dict() 17 | 18 | self.session = requests.Session() 19 | self.session.headers.update(self._headers()) 20 | 21 | def _timestamp(self): 22 | sleep = randint(1, 5) 23 | time.sleep(sleep) 24 | timestamp = int(time.time()) 25 | return timestamp 26 | 27 | def _cookies_to_dict(self): 28 | cookies_dict = {k: v for k, v in re.findall("(.*?)=(.*?);", self.cookies)} 29 | return cookies_dict 30 | 31 | def _user_agent(self): 32 | try: 33 | device_smzdm = self.cookies_dict["device_smzdm"] 34 | device_smzdm_version = self.cookies_dict["device_smzdm_version"] 35 | device_smzdm_version_code = self.cookies_dict["device_smzdm_version_code"] 36 | device_system_version = self.cookies_dict["device_system_version"] 37 | device_type = self.cookies_dict["device_type"] 38 | user_agent = f"smzdm_{device_smzdm}_V{device_smzdm_version} rv:{device_smzdm_version_code} ({device_type};{device_smzdm.capitalize()}{device_system_version};zh)smzdmapp" 39 | except KeyError: 40 | user_agent = "smzdm_android_V10.4.26 rv:866 (MI 8;Android10;zh)smzdmapp" 41 | return user_agent 42 | 43 | def _headers(self): 44 | headers = { 45 | "User-Agent": self._user_agent(), 46 | "Accept-Encoding": "gzip", 47 | "Content-Type": "application/x-www-form-urlencoded", 48 | **{ 49 | "Request_Key": f"{randint(10000000, 100000000) * 10000000000 + self._timestamp()}", 50 | "Cookie": self.cookies, 51 | }, 52 | } 53 | return headers 54 | 55 | def _web_headers(self): 56 | headers = { 57 | "Accept": "*/*", 58 | "Accept-Language": "en-US,en;q=0.9", 59 | "Connection": "keep-alive", 60 | "Cookie": self.cookies, 61 | "Referer": "https://m.smzdm.com/", 62 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.48", 63 | } 64 | return headers 65 | 66 | def _sign_data(self, data): 67 | sign_str = ( 68 | "&".join(f"{key}={value}" for key, value in sorted(data.items()) if value) 69 | + f"&key={self.SIGN_KEY}" 70 | ) 71 | sign = hashlib.md5(sign_str.encode()).hexdigest().upper() 72 | data.update({"sign": sign}) 73 | return data 74 | 75 | def data(self, extra_data=None): 76 | data = { 77 | "weixin": "1", 78 | "captcha": "", 79 | "f": self.cookies_dict["device_smzdm"], 80 | "v": self.cookies_dict["device_smzdm_version"], 81 | "touchstone_event": "", 82 | "time": self._timestamp() * 1000, 83 | "token": self.cookies_dict["sess"], 84 | } 85 | if self.sk: 86 | data.update({"sk": self.sk}) 87 | if extra_data: 88 | data.update(extra_data) 89 | return self._sign_data(data) 90 | 91 | def request(self, method, url, params=None, extra_data=None): 92 | data = self.data(extra_data) 93 | return self.session.request(method, url, params=params, data=data) 94 | 95 | 96 | if __name__ == "__main__": 97 | android_cookie_str = "" 98 | smzdm_bot = SmzdmBot(android_cookie_str) 99 | data = smzdm_bot.data() 100 | print(data) 101 | -------------------------------------------------------------------------------- /app/utils/smzdm_tasks.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | import prettytable as pt 5 | from loguru import logger 6 | from utils.smzdm_bot import SmzdmBot 7 | 8 | 9 | class SmzdmTasks: 10 | def __init__(self, bot: SmzdmBot) -> None: 11 | self.bot = bot 12 | 13 | def checkin(self): 14 | url = "https://user-api.smzdm.com/checkin" 15 | resp = self.bot.request("POST", url) 16 | if resp.status_code == 200 and int(resp.json()["error_code"]) == 0: 17 | resp_data = resp.json()["data"] 18 | checkin_num = resp_data["daily_num"] 19 | gold = resp_data["cgold"] 20 | point = resp_data["cpoints"] 21 | exp = resp_data["cexperience"] 22 | rank = resp_data["rank"] 23 | cards = resp_data["cards"] 24 | tb = pt.PrettyTable() 25 | tb.field_names = ["签到天数", "金币", "积分", "经验", "等级", "补签卡"] 26 | tb.add_row([checkin_num, gold, point, exp, rank, cards]) 27 | logger.info(f"\n{tb}") 28 | msg = f"""\n⭐签到成功{checkin_num}天 29 | 🏅金币: {gold} 30 | 🏅积分: {point} 31 | 🏅经验: {exp} 32 | 🏅等级: {rank} 33 | 🏅补签卡: {cards}""" 34 | return msg 35 | else: 36 | logger.error("Faile to sign in") 37 | msg = "Fail to login in" 38 | return msg 39 | 40 | def vip_info(self): 41 | msg = "" 42 | url = "https://user-api.smzdm.com/vip" 43 | resp = self.bot.request("POST", url) 44 | if resp.status_code == 200 and int(resp.json()["error_code"]) == 0: 45 | resp_data = resp.json()["data"] 46 | rank = resp_data["vip"]["exp_level"] 47 | exp_current_level = resp_data["vip"]["exp_current_level"] 48 | exp_level_expire = resp_data["vip"]["exp_level_expire"] 49 | tb = pt.PrettyTable() 50 | tb.field_names = ["值会员等级", "值会员经验", "值会员有效期"] 51 | tb.add_row([rank, exp_current_level, exp_level_expire]) 52 | logger.info(f"\n{tb}") 53 | msg = f""" 54 | 🏅值会员等级: {rank} 55 | 🏅值会员经验: {exp_current_level} 56 | 🏅值会员有效期: {exp_level_expire}""" 57 | return msg 58 | 59 | def all_reward(self): 60 | msg = "" 61 | url = "https://user-api.smzdm.com/checkin/all_reward" 62 | resp = self.bot.request("POST", url) 63 | if resp.status_code == 200 and int(resp.json()["error_code"]) == 0: 64 | resp_data = resp.json()["data"] 65 | if resp_data["normal_reward"]["gift"]["title"]: 66 | msg = f"\n{resp_data['normal_reward']['gift']['title']}: {resp_data['normal_reward']['gift']['content_str']}" 67 | elif resp_data["normal_reward"]["gift"]["content_str"]: 68 | msg = f"\n{resp_data['normal_reward']['gift']['content_str']}: {resp_data['normal_reward']['gift']['sub_content']}" 69 | logger.info(msg) 70 | else: 71 | logger.info("No reward today") 72 | return msg 73 | 74 | def _get_lottery_chance(self, params): 75 | headers = self.bot._web_headers() 76 | url = "https://zhiyou.smzdm.com/user/lottery/jsonp_get_current" 77 | resp = self.bot.session.get(url, headers=headers, params=params) 78 | try: 79 | result = json.loads(re.findall("({.*})", resp.text)[0]) 80 | if result["remain_free_lottery_count"] < 1: 81 | logger.warning("No lottery chance left") 82 | return False 83 | else: 84 | return True 85 | except Exception: 86 | logger.warning("No lottery chance left") 87 | return False 88 | 89 | def _draw_lottery(self, params): 90 | msg = """ 91 | 🏅没有抽奖机会 92 | """ 93 | headers = self.bot._web_headers() 94 | url = "https://zhiyou.smzdm.com/user/lottery/jsonp_draw" 95 | resp = self.bot.session.get(url, headers=headers, params=params) 96 | try: 97 | result = json.loads(re.findall("({.*})", resp.text)[0]) 98 | msg = f""" 99 | 🏅{result["error_msg"]}""" 100 | except Exception: 101 | logger.warning("Fail to parser lottery result") 102 | return msg 103 | 104 | def lottery(self): 105 | msg = """ 106 | 🏅没有抽奖机会 107 | """ 108 | timestamp = self.bot._timestamp() 109 | params = { 110 | "callback": "jQuery34100013381784658652585_{timestamp}", 111 | "active_id": "A6X1veWE2O", 112 | "_": timestamp, 113 | } 114 | if self._get_lottery_chance(params): 115 | msg = self._draw_lottery(params) 116 | return msg 117 | 118 | def extra_reward(self): 119 | continue_checkin_reward_show = False 120 | userdata_v2 = self._show_view_v2() 121 | try: 122 | for item in userdata_v2["data"]["rows"]: 123 | if item["cell_type"] == "18001": 124 | continue_checkin_reward_show = item["cell_data"][ 125 | "checkin_continue" 126 | ]["continue_checkin_reward_show"] 127 | break 128 | except Exception as e: 129 | logger.error(f"Fail to check extra reward: {e}") 130 | if not continue_checkin_reward_show: 131 | return 132 | url = "https://user-api.smzdm.com/checkin/extra_reward" 133 | resp = self.bot.request("POST", url) 134 | logger.info(resp.json()["data"]) 135 | 136 | def _show_view_v2(self): 137 | url = "https://user-api.smzdm.com/checkin/show_view_v2" 138 | resp = self.bot.request("POST", url) 139 | if resp.status_code == 200 and int(resp.json()["error_code"]) == 0: 140 | return resp.json() 141 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | smzdm_bot: 4 | image: enwaiax/smzdm_bot 5 | container_name: smzdm_bot 6 | restart: on-failure 7 | logging: 8 | driver: "json-file" 9 | options: 10 | max-size: "1m" 11 | max-file: "1" 12 | env_file: 13 | - .env 14 | # volumes: 15 | # - ./app/config/config.toml:/smzdm_bot/config/config.toml 16 | -------------------------------------------------------------------------------- /smzdm_ql.py: -------------------------------------------------------------------------------- 1 | """ 2 | 什么值得买自动签到脚本 3 | 项目地址: https://github.com/Chasing66/smzdm_bot 4 | 0 9 * * * smzdm_ql.py 5 | const $ = new Env("什么值得买签到"); 6 | """ 7 | 8 | import os 9 | from pathlib import Path 10 | 11 | ql_repo_dir = Path("/ql/data/repo/") 12 | repo_name = "Chasing66_smzdm_bot" 13 | repo_dir = Path(ql_repo_dir, repo_name) 14 | 15 | 16 | def main(): 17 | os.system( 18 | f"cd {str(repo_dir)}; pip3 install -qr app/requirements.txt; python3 app/main.py" 19 | ) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | --------------------------------------------------------------------------------