diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml new file mode 100644 index 0000000..807f31d --- /dev/null +++ b/.github/workflows/build-publish.yml @@ -0,0 +1,47 @@ +name: Build and Publish Wheel + +on: + push: + tags: + - "*" + +jobs: + build_and_publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rye + shell: bash + env: + RYE_INSTALL_OPTION: "--yes" + RYE_VERSION: 0.43.0 + run: | + curl -sSf https://rye.astral.sh/get | bash + echo 'source "$HOME/.rye/env"' >> $GITHUB_PATH + + + - name: Install dependencies + run: | + rye sync + . .venv/bin/activate + + - name: Build Wheel + run: | + rye build --clean + + - name: Upload Wheel as Release Asset + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: dist/*.* + + - name: Publish package distributions to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + with: + password: ${{ secrets.PYPI_API_TOKEN }} + # password: ${{ secrets.TEST_PYPI_API_TOKEN }} + # repository-url: https://test.pypi.org/legacy/ diff --git a/.gitignore b/.gitignore index 1d74e21..9378b34 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,182 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + .vscode/ + +**/__pycache__ + +# venv +.venv + +# ruff linter +.ruff_cache/ + +# pytest +.pytest_cache/ + +# example +.env +.env.* +env.sh + +**/*.csv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..173150a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +ci: + autoupdate_schedule: monthly +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.8.3 + hooks: + # Run the linter. + - id: ruff + # Run the formatter. + - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-merge-conflict + - id: debug-statements + - id: fix-byte-order-marker + - id: trailing-whitespace + - id: end-of-file-fixer diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..251b350 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10.16 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4cb55d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Jetsung Chan + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index dd222db..6549f15 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,107 @@ `.cn`, `.top` 预删除的域名获取。 -## 支持的域名后缀 -- `.cn` -- `.top` +## 使用方法 +### 1. 安装依赖包: +- 方式一:通过 pypi +```bash +pip install predeldomain +``` +- 方式二:通过代码仓库 +```bash +pip install git+git@github.com:jetsung/predeldomain.git +``` +- 方式三:通过本地仓库 +```bash +pip install -e . +``` +- 方式四:通过 wheel 包 +```bash +pip install predeldomain-X.X.X-py3-none-any.whl +``` -## 使用 +### 2. 使用帮助 + +```bash +» predeldomain --help +usage: predeldomain [-h] [-l [1-10]] [-m {1,2,3}] [-s {cn,top}] [-t {text,json}] [-w WHOIS] + +The domain name to be pre-deleted. + +options: + -h, --help show this help message and exit + -l [1-10], --length [1-10] + Length: 1 to 10 + -m {1,2,3}, --mode {1,2,3} + Mode: 1. Alphanumeric, 2. Numeric, 3. Alphabetic + -s {cn,top}, --suffix {cn,top} + Suffix: 'cn' or 'top' + -t {text,json}, --type {text,json} + Save type: 'text' or 'json' + -w WHOIS, --whois WHOIS + Whois: whois, isp, none +``` +1. length: 长度,不含后缀 +2. mode: 模式, 1. 数字 + 字母, 2. 数字, 3. 字母 +3. suffix: 域名后缀, 'cn' 或者 'top' +4. type: 保存类型, 'text' 或者 'json' (数据保存和发送通知的格式) +5. whois: whois, isp,查询可用的方式。`留空`,则不查询,而是直接根据官网提供的数据判断;`whois`,则使用 `whois` 库查询;`isp` 则使用腾讯云的 API 查询。 +结果将会通过 PUSH 通知,和保存到本地文件。本地文件将会以 `后缀_日期.log` 的格式保存(`_next`则是明天及以后预删除的域名)。 + +### 3. PUSH 通知 +当前仅支持 [**Lark**](https://www.larksuite.com/) 以及 [**PushDeer**](http://www.pushdeer.com/)。依赖 [**ipush 库**](https://github.com/idevsig/pypush),可自行添加其它渠道。 +需要设置环境变量 +```bash +# Lark +export LARK_TOKEN="" +export LARK_SECRET="" + +# PushDeer +export PUSHDEER_TOKEN="" +``` + +## 开发 + +### 1. 前置开发环境 + +1. 使用 [**Rye**](https://rye-up.com/) 作为包管理工具 + +### 2. 开发流程 + +1. 安装依赖包: + +```bash +# 同步 +rye sync +``` + +2. 代码检测与格式化: + +```bash +# 检测 +rye run check + +# 格式化 +rye run format +``` + +3. 单元测试: + +```bash +# rye test +rye run tests + +# pytest +python -m pytest + +# 打印测试报告 +python -m pytest -s +``` ## 仓库镜像 -- https://git.jetsung.com/idev/predomain -- https://framagit.org/idev/predomain -- https://codeup.aliyun.com/jetsung/idev/predomain +- https://git.jetsung.com/idev/predeldomain +- https://framagit.org/idev/predeldomain +- https://github.com/idevsig/predeldomain +- https://gitcode.com/idev/predeldomain diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2673a7d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,98 @@ +[project] +name = "predeldomain" +version = "0.1.0" +description = "预删除域名查询" +authors = [ + { name = "Jetsung Chan", email = "jetsungchan@gmail.com" } +] +dependencies = [ + "ipush>=0.6.0", + "whois>=1.20240129.2", + "pre-commit>=4.0.1", + "ruff>=0.8.3", +] +readme = "README.md" +keywords = ["domain"] +requires-python = ">= 3.10" +classifiers = [ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Natural Language :: Chinese (Simplified)', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: Implementation :: CPython', + 'Topic :: Communications :: Email', + 'Topic :: Software Development :: Libraries', +] + +[project.urls] +Homepage = "https://git.jetsung.com/idev/predeldomain" +Documentation = "https://framagit.org/idev/predeldomain" +Repository = "https://framagit.org/idev/predeldomain.git" + +[project.scripts] +predeldomain = "predeldomain:predeldomain.main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.rye] +managed = true +dev-dependencies = [ + "ruff>=0.1.9", + "pre-commit>=4.0.1", + "pytest>=8.3.4", +] + +[tool.rye.scripts] +tests = { cmd = "python -m pytest" } +format = { cmd = "python -m ruff format ." } +check = { cmd = "python -m ruff check . --fix" } + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel] +packages = ["src/predeldomain"] + + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = [ + "error", +] + +[tool.ruff] +src = ["src"] +fix = true +show-fixes = true +# line-length = 100 + +[tool.ruff.format] +quote-style = "single" +# indent-style = "tab" +docstring-code-format = true + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "E", # pycodestyle error + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "W", # pycodestyle warning +] +ignore = ["E501"] + +[tool.ruff.lint.isort] +force-single-line = true +order-by-type = false diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..046f191 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,61 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +certifi==2023.11.17 + # via requests +cfgv==3.4.0 + # via pre-commit +charset-normalizer==3.3.2 + # via requests +distlib==0.3.8 + # via virtualenv +exceptiongroup==1.2.2 + # via pytest +filelock==3.13.1 + # via virtualenv +identify==2.5.33 + # via pre-commit +idna==3.6 + # via requests +iniconfig==2.0.0 + # via pytest +ipush==0.6.0 + # via predeldomain +lxml==4.9.4 + # via ipush +nodeenv==1.8.0 + # via pre-commit +packaging==23.2 + # via pytest +platformdirs==4.1.0 + # via virtualenv +pluggy==1.5.0 + # via pytest +pre-commit==4.0.1 + # via predeldomain +pytest==8.3.4 +pyyaml==6.0.1 + # via pre-commit +requests==2.31.0 + # via ipush +ruff==0.8.3 + # via predeldomain +setuptools==69.0.3 + # via nodeenv +tomli==2.2.1 + # via pytest +urllib3==2.1.0 + # via requests +virtualenv==20.25.0 + # via pre-commit +whois==1.20240129.2 + # via predeldomain diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..8670a5b --- /dev/null +++ b/requirements.lock @@ -0,0 +1,48 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +certifi==2023.11.17 + # via requests +cfgv==3.4.0 + # via pre-commit +charset-normalizer==3.3.2 + # via requests +distlib==0.3.9 + # via virtualenv +filelock==3.16.1 + # via virtualenv +identify==2.6.3 + # via pre-commit +idna==3.6 + # via requests +ipush==0.6.0 + # via predeldomain +lxml==4.9.4 + # via ipush +nodeenv==1.9.1 + # via pre-commit +platformdirs==4.3.6 + # via virtualenv +pre-commit==4.0.1 + # via predeldomain +pyyaml==6.0.2 + # via pre-commit +requests==2.31.0 + # via ipush +ruff==0.8.3 + # via predeldomain +urllib3==2.1.0 + # via requests +virtualenv==20.28.0 + # via pre-commit +whois==1.20240129.2 + # via predeldomain diff --git a/src/predeldomain/__init__.py b/src/predeldomain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/predeldomain/__main__.py b/src/predeldomain/__main__.py new file mode 100644 index 0000000..4f1749b --- /dev/null +++ b/src/predeldomain/__main__.py @@ -0,0 +1,4 @@ +from .predeldomain import main + +if __name__ == '__main__': + main() diff --git a/src/predeldomain/predeldomain.py b/src/predeldomain/predeldomain.py new file mode 100644 index 0000000..b09ad62 --- /dev/null +++ b/src/predeldomain/predeldomain.py @@ -0,0 +1,66 @@ +import argparse +import sys + +from predeldomain.provider.entry import run + + +def parse_arguments(): + parser = argparse.ArgumentParser(description='The domain name to be pre-deleted.') + + parser.add_argument( + '-l', + '--length', + type=int, + default=3, + metavar='[1-10]', + choices=range(1, 11), + help='Length: 1 to 10', + ) + parser.add_argument( + '-m', + '--mode', + type=int, + choices=[1, 2, 3], + default=1, + help='Mode: 1. Alphanumeric, 2. Numeric, 3. Alphabetic', + ) + parser.add_argument( + '-s', + '--suffix', + type=str, + choices=['cn', 'top'], + default='cn', + help="Suffix: 'cn' or 'top'", + ) + parser.add_argument( + '-t', + '--type', + type=str, + choices=['text', 'json'], + default='text', + help="Save type: 'text' or 'json'", + ) + parser.add_argument( + '-w', + '--whois', + type=str, + default='', + help='Whois: whois, isp, none', + ) + + args = parser.parse_args() + return args + + +def main(): + try: + args = parse_arguments() + run(args) + + except Exception as e: + print(f'Error: {e}', file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/src/predeldomain/provider/entry.py b/src/predeldomain/provider/entry.py new file mode 100644 index 0000000..ed53d7a --- /dev/null +++ b/src/predeldomain/provider/entry.py @@ -0,0 +1,118 @@ +import json +from datetime import datetime +from os import environ + +from ipush import Lark +from ipush import PushDeer + +from predeldomain.provider.provider import Provider +from predeldomain.provider.provider_cn import CN +from predeldomain.provider.provider_top import TOP + + +def run(args): + """ + 主函数 + """ + + functions = {'top': TOP, 'cn': CN} + + # print( + # f'Domain Suffix: {args.suffix}, Length: {args.length}, Mode: {args.mode}, Whois: {args.whois}' + # ) + provider = functions.get(args.suffix, Provider)(args.length, args.mode, args.whois) + provider.entry() + + data_list = provider.data_all() + write_log(data_list, args.suffix, args.type) + + notify(data_list, args.suffix) + + +def write_log(data_list, suffix, type='text'): + """ + 处理数据 + """ + + if len(data_list) == 0: + return + + today = datetime.now().date() + file_log = f'{suffix}_{today}.log' + file_log_next = f'{suffix}_{today}_next.log' + + # print(data_list) + + if type == 'text': + data_str = f'.{suffix}\n'.join(data_list[0]) + f'.{suffix}\n' + with open(file_log, 'w') as f: + f.write(data_str) + + for i in range(1, len(data_list)): + if i == 1: + wmode = 'w' + else: + wmode = 'a' + + data_str = f'.{suffix}\n'.join(data_list[i]) + f'.{suffix}\n' + with open(file_log_next, wmode) as f: + f.write(f'===========================================\n{data_str}') + + elif type == 'json': + data_json = json.dumps(data_list[0], indent=4) + with open(file_log, 'w') as f: + f.write(data_json) + + merged_list = sum(data_list[1:], []) + data_json = json.dumps(merged_list, indent=4) + with open(file_log_next, 'w') as f: + f.write(data_json) + + +def notify(data_list, suffix): + """ + 发送通知 + """ + if len(data_list) == 0: + return + + content = '' + content_markdown = '' + content_text = '' + # 今天数据 + if len(data_list[0]) > 0: + content = '\n'.join(data_list[0]) + content_markdown = f'**域名 `{suffix}` 今天过期:**\n```bash\n{content}\n```' + content_text = f'域名 {suffix} 今天过期:\n{content}\n' + + # 明天数据 + content_next = '' + content_next_markdown = '' + content_next_text = '' + if len(data_list) > 1: + content_next = '\n'.join(data_list[1]) + content_next_markdown = ( + f'**域名 `{suffix}` 明天过期:**\n```bash\n{content_next}\n```' + ) + content_next_text = f'域名 {suffix} 明天过期:\n{content_next}\n' + + # 发送通知 PushDeer + pushdeer_token = environ.get('PUSHDEER_TOKEN', '') + if pushdeer_token != '': + notify = PushDeer(pushdeer_token) + if content: + notify.settype('markdown').send(content_markdown) + + if content_next: + notify.settype('markdown').send(content_next_markdown) + + # 发送通知 Lark + lark_token = environ.get('LARK_TOKEN', '') + lark_secret = environ.get('LARK_SECRET', '') + if lark_token != '' and lark_secret != '': + notify = Lark(lark_token, lark_secret) + if content: + notify.send(content_text) + + if content_next: + notify.send(content_next_text) diff --git a/src/predeldomain/provider/provider.py b/src/predeldomain/provider/provider.py new file mode 100644 index 0000000..2d2befe --- /dev/null +++ b/src/predeldomain/provider/provider.py @@ -0,0 +1,156 @@ +import os +import re +import sys +from datetime import datetime + +import requests +import whois + +from predeldomain.provider.service import Mode + +""" +Provider 提供者 +""" + + +class Provider: + data = [] + + whois_tencent_url = 'https://dnspod.cloud.tencent.com/cgi/capi?action=DescribeWhoisInfoSpecial&csrfCode=&innerCapiMark=1' + + def __init__( + self, + length=3, + mode=Mode.ALPHABETIC.value, + whois='', + ): + self.length = length + self.mode = mode + self.whois = whois + + def is_domain_available(self, domain: str) -> bool: + """ + 判断是否可注册 + """ + return False + + def entry(self): # noqa: B027 + """ + 主函数 + """ + pass + + def data_all(self): + """ + 获取所有数据 + """ + return self.data + + def data_today(self): + """ + 获取今日数据 + """ + return self.data[0] if len(self.data) > 0 else [] + + def data_future(self): + """ + 获取未来数据 + """ + return self.data[1:] if len(self.data) > 1 else [] + + def match_mode(self, data): + """ + 匹配模式 + """ + if self.mode == Mode.ALPHANUMERIC.value and not re.match( + r'^[a-zA-Z0-9]+$', data + ): + return False + if self.mode == Mode.NUMERIC.value and not re.match(r'^[0-9]+$', data): + return False + if self.mode == Mode.ALPHABETIC.value and not re.match(r'^[a-zA-Z]+$', data): + return False + return True + + def remove_file(self, file_name: str): + """ + 删除文件 + """ + if os.path.isfile(file_name): + os.remove(file_name) + + def should_download_file(self, file_name: str): + """ + 检查是否需要下载文件 + """ + if not os.path.isfile(file_name): + return True + file_time = datetime.fromtimestamp(os.path.getmtime(file_name)) + return file_time.date() != datetime.now().date() + + def whois_available(self, domain): + """ + 通过 Whois 判断是否可注册 + """ + try: + w = whois.query(domain) + # return True if w.available else False + if w is None: + return True + else: + return False + except Exception as e: + print(f'Error: {domain}, {e}', file=sys.stderr) + return False + + def isp_available(self, domain): + """ + 通过 ISP 判断是否可注册 + """ + + data = { + 'Version': '2018-08-08', + 'serviceType': 'domain', + 'api': 'DescribeWhoisInfoSpecial', + 'DomainName': domain, + 'dpNodeCustomClientIPField': 'RealClientIp', + } + + headers = { + 'accept': 'application/json, text/javascript, */*; q=0.01', + 'accept-encoding': 'gzip, deflate, br, zstd', + 'accept-language': 'en-US,en;q=0.9', + 'content-length': '149', + 'content-type': 'application/json; charset=UTF-8', + # 'cookie': '__root_domain_v=.tencent.com; _qddaz=QD.725133824044670; hy_user=a_98e5efef527597446e27dcffc370ae58; hy_token=R4Fso9Hx4m6w7zCdXsxx0cMFpR5yqUGgMw/q9ioxg1Vcrpd46wehlnDrLKYPWyfwn0yPhOrq1LckTgoLv0p7dA==; hy_source=web; qcloud_uid=oJv0qdK_ZSks; language=zh; qcstats_seo_keywords=%E5%93%81%E7%89%8C%E8%AF%8D-%E5%93%81%E7%89%8C%E8%AF%8D-%E7%99%BB%E5%BD%95; _ga=GA1.2.261890131.1733898849; _gcl_au=1.1.1975438295.1733898849; loginType=wx; sid=b8b508544870b6d77b724ffb9ccad6cc; trafficParams=***%24%3Btimestamp%3D1733987618427%3Bfrom_type%3Dserver%3Btrack%3Da49c89bc-d795-4377-a9ef-098cdcc67e3d%3B%24***; qcloud_visitId=22dcaec137f7d8ed64f5a131576e916e; _gat=1; qcmainCSRFToken=SkzN-My9N1g; intl=; qcloud_outsite_refer=https://whois.cloud.tencent.com; qcloud_from=qcloud.inside.whois-1734107656341; dp.sess=b80f551d2e83e8aad0505b61e27b85a22e69538b29716c4a4a', + 'origin': 'https://whois.cloud.tencent.com', + 'priority': 'u=1, i', + 'referer': 'https://whois.cloud.tencent.com/', + 'sec-ch-ua': '"Chromium";v="131", "Not_A Brand";v="24"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Linux"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-site', + 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + } + + response = requests.post(self.whois_tencent_url, json=data, headers=headers) + + try: + if response.status_code != 200: + raise ValueError(f'status code {response.status_code}') + + resp = response.json() + if response.status_code != 200: + raise ValueError(f'status code is: {response.status_code}') + if 'code' not in resp or resp['code'] != 0: + raise ValueError(f'code not in resp: {resp}') + if 'message' in resp and '未注册' in resp['message']: + return True + else: + return False + + except Exception as e: + print(f'Error: find domain: {domain}, err:{e}') + return False diff --git a/src/predeldomain/provider/provider_cn.py b/src/predeldomain/provider/provider_cn.py new file mode 100644 index 0000000..24d9160 --- /dev/null +++ b/src/predeldomain/provider/provider_cn.py @@ -0,0 +1,78 @@ +import requests + +from predeldomain.provider.provider import Provider + + +class CN(Provider): + file_urls = { + 'today': 'https://www.cnnic.cn/NMediaFile/domain_list/1todayDel.txt', + 'tomorrow': 'https://www.cnnic.cn/NMediaFile/domain_list/future1todayDel.txt', + 'after_tomorrow': 'https://www.cnnic.cn/NMediaFile/domain_list/future2todayDel.txt', + } + + def download_txt(self, url): + """ + 下载 TXT 文件 + """ + response = requests.get(url) + if response.status_code == 200: + return response.text + else: + raise Exception( + f'Failed to download TXT file from {url}, status code: {response.status_code}' + ) + + def is_domain_available(self, domain): + """ + 判断是否可注册 + """ + full_domain = f'{domain}.cn' + if self.whois == 'isp': + return self.isp_available(full_domain) + elif self.whois == 'whois': + return self.whois_available(full_domain) + else: + return True + + def _process_response(self, response, is_today=False): + """ + 处理响应数据并返回符合条件的域名列表 + """ + domain_list = [] + for line in response.splitlines(): + domain = ( + line.strip('[]').strip().replace('.cn', '') + ) # 去除中括号,多余空格和.cn后缀 + if not self.match_mode(domain): # 检查域名匹配模式 + continue + if len(domain) <= self.length: + if is_today: + if self.is_domain_available(domain): + domain_list.append(domain) + else: + domain_list.append(domain) + return domain_list + + def entry(self): + """ + 主函数 + """ + # 下载并处理今天的数据 + today_resp = self.download_txt(self.file_urls['today']) + data_list = self._process_response(today_resp, True) + + # 下载并处理明天和后天的数据 + data_tomorrow = [] + data_after_tomorrow = [] + + tomorrow_resp = self.download_txt(self.file_urls['tomorrow']) + data_tomorrow = self._process_response(tomorrow_resp) + + after_tomorrow_resp = self.download_txt(self.file_urls['after_tomorrow']) + data_after_tomorrow = self._process_response(after_tomorrow_resp) + + # 排序结果 + data_list.sort() + data_tomorrow.sort() + data_after_tomorrow.sort() + self.data = [data_list, data_tomorrow, data_after_tomorrow] diff --git a/src/predeldomain/provider/provider_top.py b/src/predeldomain/provider/provider_top.py new file mode 100644 index 0000000..b148d9a --- /dev/null +++ b/src/predeldomain/provider/provider_top.py @@ -0,0 +1,79 @@ +import csv +from datetime import datetime +from datetime import timedelta +from io import StringIO + +import requests + +from predeldomain.provider.provider import Provider + + +class TOP(Provider): + csv_file = 'top.csv' + file_url = 'https://www.nic.top/upload/top/dellist.csv' + + def download_csv(self): + """ + 定义下载 CSV 文件的函数 + """ + response = requests.get(self.file_url) + if response.status_code == 200: + return response.content + else: + raise Exception( + f'Failed to download CSV file, status code: {response.status_code}' + ) + + def is_domain_available(self, domain): + """ + 判断是否可注册 + """ + + if self.whois == 'isp': + params = {'domainName': domain} + response = requests.post( + 'https://www.nic.top/cn/whoischeck.asp', data=params + ) + return 'is available' in response.text + elif self.whois == 'whois': + return self.whois_available(f'{domain}.top') + else: + return True + + def entry(self): + """ + 主函数 + """ + + resp_content = self.download_csv() + # 将响应内容解码为文本 + content = resp_content.decode('gbk') + + # 使用 StringIO 创建类文件对象 + csv_file = StringIO(content) + reader = csv.reader(csv_file) + + self.remove_file(self.csv_file) + + next(reader) # 跳过 CSV 文件的头部 + + data_list = [] + data_next = [] + + for row in reader: + data = row[0].replace('.top', '') + if not self.match_mode(data): + continue + if len(data) <= self.length: + given_time = datetime.strptime(row[1], '%Y/%m/%d %H:%M') + if given_time < datetime.now(): + if self.is_domain_available(data): + data_list.append(data) + else: + data_list.append(data) + elif given_time < datetime.now() + timedelta(days=1): + data_next.append(data) + + data_list.sort() + data_next.sort() + self.data = [data_list, data_next] diff --git a/src/predeldomain/provider/service.py b/src/predeldomain/provider/service.py new file mode 100644 index 0000000..d139a85 --- /dev/null +++ b/src/predeldomain/provider/service.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class Mode(Enum): + ALPHANUMERIC = 1 + NUMERIC = 2 + ALPHABETIC = 3