├── .gitignore ├── FN_extend.js ├── FN_extend.py ├── FNplus.js ├── FNplus.py ├── Pipfile ├── Pipfile.lock ├── README.md ├── requirements.txt ├── templates └── default.html └── utils ├── __init__.py ├── exception.py ├── freenom.py ├── mail.py └── settings.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | !.vscode/settings.json 3 | !.vscode/tasks.json 4 | !.vscode/launch.json 5 | !.vscode/extensions.json 6 | *.code-workspace 7 | 8 | # Local History for Visual Studio Code 9 | .history/ 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | *.py,cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | cover/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | db.sqlite3 72 | db.sqlite3-journal 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | .pybuilder/ 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | # For a library or package, you might want to ignore these files since the code is 97 | # intended to run in multiple environments; otherwise, check them in: 98 | # .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .venv 120 | env/ 121 | venv/ 122 | ENV/ 123 | env.bak/ 124 | venv.bak/ 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ 143 | 144 | # pytype static type analyzer 145 | .pytype/ 146 | 147 | # Cython debug symbols 148 | cython_debug/ 149 | -------------------------------------------------------------------------------- /FN_extend.js: -------------------------------------------------------------------------------- 1 | // @grant nodejs 2 | console.log('⏳ 初始化安装依赖中......'); 3 | $exec('wget https://raw.githubusercontent.com/Oreomeow/freenom-py/main/requirements.txt -O requirements.txt && pip3 install -r requirements.txt', { 4 | cwd: './script/Shell', 5 | timeout: 0, 6 | cb(data, error) { 7 | error ? console.error(error) : console.log(data); 8 | }, 9 | }); 10 | console.log('⏳ 开始拉取 git 仓库 Oreomeow/freenom-py'); 11 | $exec('git clone https://github.com/Oreomeow/freenom-py.git', { 12 | cwd: './script/Shell', 13 | timeout: 0, 14 | cb(data, error) { 15 | error ? console.error(error) : console.log(data); 16 | }, 17 | }); 18 | console.log('⏳ 开始执行 FN_extend.py'); 19 | $exec('python3 FN_extend.py', { 20 | cwd: './script/Shell/freenom-py', 21 | timeout: 0, 22 | env: { 23 | FN_ID: $store.get('FN_ID', 'string'), 24 | FN_PW: $store.get('FN_PW', 'string'), 25 | MAIL_USER: $store.get('MAIL_USER', 'string'), 26 | MAIL_ADDRESS: $store.get('MAIL_ADDRESS', 'string'), 27 | MAIL_PW: $store.get('MAIL_PW', 'string'), 28 | MAIL_HOST: $store.get('MAIL_HOST', 'string'), 29 | MAIL_PORT: $store.get('MAIL_PORT', 'string'), 30 | MAIL_TO: $store.get('MAIL_TO', 'string'), 31 | }, 32 | cb(data, error) { 33 | error ? console.error(error) : console.log(data); 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /FN_extend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | """ 3 | Author: shuai93 4 | Modifier: Oreo 5 | Date: Wed Aug 11 10:15:41 UTC 2021 6 | cron: 25 7 */10 * * 7 | new Env('Freenom 续期邮件版'); 8 | ------------ 9 | 环境变量说明 示例 10 | FN_ID: Freenom 用户名 1234567890@gmail.com 11 | FN_PW: Freenom 密码 12345678 12 | MAIL_USER: 发件人邮箱用户名 address@vip.qq.com 或 123456@qq.com 13 | MAIL_ADDRESS: 发件人邮箱地址 address@vip.qq.com 或 123456@qq.com 14 | MAIL_PW: 发件人邮箱授权码 xxxxxxxxxxxxxxxx 看下方链接 15 | * MAIL_HOST: 发件人邮箱服务器 smtp.qq.com 不填默认为这个 16 | * MAIL_PORT: 邮箱服务器端口 465 不填默认为这个 17 | MAIL_TO: 收件人邮箱可与发件人相同 address@vip.qq.com 或 123456@qq.com 18 | 19 | 填写总参考:https://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=369 20 | ------------ 21 | 依赖模块说明 22 | pip install -r requirements.txt / pip3 install -r requirements.txt 23 | """ 24 | 25 | from utils.exception import CustomException 26 | from utils.freenom import FreeNom 27 | from utils.mail import EmailPoster 28 | from utils.settings import ( 29 | FN_ID, 30 | FN_PW, 31 | MAIL_ADDRESS, 32 | MAIL_HOST, 33 | MAIL_PORT, 34 | MAIL_PW, 35 | MAIL_TO, 36 | MAIL_USER, 37 | ) 38 | 39 | 40 | def mask_data(data: str) -> str: 41 | return "".join( 42 | ["*" if i > 2 and i < len(data) - 3 else data[i] for i in range(len(data))] 43 | ) 44 | 45 | 46 | def main(): 47 | envs = [ 48 | FN_ID, 49 | FN_PW, 50 | MAIL_USER, 51 | MAIL_ADDRESS, 52 | MAIL_PW, 53 | MAIL_HOST, 54 | MAIL_PORT, 55 | MAIL_TO, 56 | ] 57 | print("配置信息(脱敏后):") 58 | print([mask_data(env) if isinstance(env, str) else env for env in envs]) 59 | if not all(envs): 60 | raise CustomException("参数缺失") 61 | 62 | body = { 63 | "subject": "FreeNom 自动续期", 64 | "to": [MAIL_TO], 65 | } 66 | try: 67 | results = FreeNom().run() 68 | body["payload"] = {"results": results, "user": FN_ID} 69 | except CustomException as e: 70 | body["body"] = e.message 71 | 72 | EmailPoster().send(data=body) 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /FNplus.js: -------------------------------------------------------------------------------- 1 | // @grant nodejs 2 | console.log('⏳ 初始化安装推送模块中......'); 3 | $exec('wget https://raw.githubusercontent.com/whyour/qinglong/master/sample/notify.py notify.py', { 4 | cwd: './script/Shell', 5 | timeout: 0, 6 | cb(data, error) { 7 | error ? console.error(error) : console.log(data); 8 | }, 9 | }); 10 | console.log('⏳ 开始执行 FNplus.py'); 11 | $exec('python3 https://raw.githubusercontent.com/Oreomeow/freenom-py/main/FNplus.py', { 12 | cwd: './script/Shell', 13 | timeout: 0, 14 | env: { 15 | FN_ID: $store.get('FN_ID', 'string'), // Freenom 用户名 16 | FN_PW: $store.get('FN_PW', 'string'), // Freenom 密码 17 | BARK: $store.get('BARK', 'string'), // bark IP 或设备码,例:https://api.day.app/xxxxxx 18 | DD_BOT_SECRET: $store.get('DD_BOT_SECRET', 'string'), // 钉钉机器人的 DD_BOT_SECRET 19 | DD_BOT_TOKEN: $store.get('DD_BOT_TOKEN', 'string'), // 钉钉机器人的 DD_BOT_TOKEN 20 | PUSH_KEY: $store.get('PUSH_KEY', 'string'), // server 酱的 PUSH_KEY,兼容旧版与 Turbo 版 21 | PUSH_PLUS_TOKEN: $store.get('PUSH_PLUS_TOKEN', 'string'), // push+ 微信推送的用户令牌 22 | QYWX_AM: $store.get('QYWX_AM', 'string'), // 企业微信应用的 QYWX_AM,参考 http://note.youdao.com/s/HMiudGkb,依次填入 corpid, corpsecret, touser(注:多个成员ID使用 | 隔开), agentid, media_id(选填,不填默认文本消息类型) 23 | TG_BOT_TOKEN: $store.get('TG_BOT_TOKEN', 'string'), // tg 机器人的 TG_BOT_TOKEN 24 | TG_USER_ID: $store.get('TG_USER_ID', 'string'), // tg 机器人的 TG_USER_ID 25 | TG_PROXY_IP: $store.get('TG_PROXY_HOST', 'string'), // tg 机器人的 TG_PROXY_IP,例:127.0.0.1,可不填 26 | TG_PROXY_PORT: $store.get('TG_PROXY_PORT', 'string'), // tg 机器人的 TG_PROXY_PORT,例:1080,可不填 27 | }, 28 | cb(data, error) { 29 | error ? console.error(error) : console.log(data); 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /FNplus.py: -------------------------------------------------------------------------------- 1 | """ 2 | cron: 25 7 */10 * * 3 | new Env('Freenom 续期消息版'); 4 | """ 5 | 6 | import argparse 7 | import os 8 | import re 9 | 10 | import requests 11 | 12 | # 登录地址 13 | LOGIN_URL = "https://my.freenom.com/dologin.php" 14 | 15 | # 域名状态地址 16 | DOMAIN_STATUS_URL = "https://my.freenom.com/domains.php?a=renewals" 17 | 18 | # 域名续期地址 19 | RENEW_DOMAIN_URL = "https://my.freenom.com/domains.php?submitrenewals=true" 20 | 21 | # token 正则 22 | token_ptn = re.compile('name="token" value="(.*?)"', re.I) 23 | 24 | # 域名信息正则 25 | domain_info_ptn = re.compile( 26 | r'(.*?)[^<]+[^<]+.*?', 27 | re.I, 28 | ) 29 | 30 | # 登录状态正则 31 | login_status_ptn = re.compile('Logout', re.I) 32 | 33 | 34 | def qlnotify(desp): 35 | cur_path = os.path.abspath(os.path.dirname(__file__)) 36 | if os.path.exists(f"{cur_path}/notify.py"): 37 | try: 38 | from notify import send # type: ignore 39 | except Exception: 40 | print("加载通知服务失败~") 41 | else: 42 | send("Freenom 续期", desp) 43 | 44 | 45 | class FreeNom: 46 | def __init__(self, username: str, password: str): 47 | self._u = username 48 | self._p = password 49 | self._s = requests.session() 50 | self._s.headers.update( 51 | { 52 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/79.0.3945.130 Safari/537.36" 53 | } 54 | ) 55 | 56 | def _login(self) -> bool: 57 | self._s.headers.update( 58 | { 59 | "content-type": "application/x-www-form-urlencoded", 60 | "referer": "https://my.freenom.com/clientarea.php", 61 | } 62 | ) 63 | r = self._s.post(LOGIN_URL, data={"username": self._u, "password": self._p}) 64 | return r.status_code == 200 65 | 66 | def renew(self) -> str: 67 | msg = "" 68 | # login 69 | if not self._login(): 70 | msg = "login failed" 71 | print(msg) 72 | return msg 73 | 74 | # check domain status 75 | self._s.headers.update({"referer": "https://my.freenom.com/clientarea.php"}) 76 | r = self._s.get(DOMAIN_STATUS_URL) 77 | 78 | # login status check 79 | if not re.search(login_status_ptn, r.text): 80 | msg = "get login status failed" 81 | print(msg) 82 | return msg 83 | 84 | # page token 85 | match = re.search(token_ptn, r.text) 86 | if not match: 87 | msg = "get page token failed" 88 | print(msg) 89 | return msg 90 | token = match[1] 91 | 92 | # domains 93 | domains = re.findall(domain_info_ptn, r.text) 94 | 95 | # renew domains 96 | for domain, days, renewal_id in domains: 97 | if int(days) < 14: 98 | self._s.headers.update( 99 | { 100 | "referer": f"https://my.freenom.com/domains.php?a=renewdomain&domain={renewal_id}", 101 | "content-type": "application/x-www-form-urlencoded", 102 | } 103 | ) 104 | r = self._s.post( 105 | RENEW_DOMAIN_URL, 106 | data={ 107 | "token": token, 108 | "renewalid": renewal_id, 109 | f"renewalperiod[{renewal_id}]": "12M", 110 | "paymentmethod": "credit", 111 | }, 112 | ) 113 | result = ( 114 | f"{domain} 续期成功" 115 | if r.text.find("Order Confirmation") != -1 116 | else f"{domain} 续期失败" 117 | ) 118 | print(result) 119 | msg += result + "\n" 120 | result = f"{domain} 还有 {days} 天续期" 121 | print(result) 122 | msg += result + "\n" 123 | return msg 124 | 125 | 126 | def main(): 127 | parser = argparse.ArgumentParser() 128 | parser.add_argument("-u", "--username", type=str) 129 | parser.add_argument("-p", "--password", type=str) 130 | args = parser.parse_args() 131 | username = args.username or os.getenv("FN_ID") 132 | password = args.password or os.getenv("FN_PW") 133 | 134 | if not username or not password: 135 | print("你没有添加任何账户") 136 | exit(1) 137 | 138 | user_list = username.strip().split() 139 | passwd_list = password.strip().split() 140 | 141 | if len(user_list) != len(passwd_list): 142 | print("账户与密码不匹配") 143 | exit(1) 144 | 145 | for i in range(len(user_list)): 146 | freenom = FreeNom(user_list[i], passwd_list[i]) 147 | qlnotify(freenom.renew()) 148 | 149 | 150 | if __name__ == "__main__": 151 | main() 152 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "==2.25.1" 10 | jinja2 = "*" 11 | 12 | [requires] 13 | python_version = "3.6" 14 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "8d5181e308f3fd3e886ad88e9a5ea526e926a967f7abb7281e5d7a92cfdbd17f" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", 22 | "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" 23 | ], 24 | "version": "==2020.12.5" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 29 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 30 | ], 31 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 32 | "version": "==4.0.0" 33 | }, 34 | "idna": { 35 | "hashes": [ 36 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 37 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 38 | ], 39 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 40 | "version": "==2.10" 41 | }, 42 | "jinja2": { 43 | "hashes": [ 44 | "sha256:2f2de5285cf37f33d33ecd4a9080b75c87cd0c1994d5a9c6df17131ea1f049c6", 45 | "sha256:ea8d7dd814ce9df6de6a761ec7f1cac98afe305b8cdc4aaae4e114b8d8ce24c5" 46 | ], 47 | "index": "pypi", 48 | "version": "==3.0.0" 49 | }, 50 | "markupsafe": { 51 | "hashes": [ 52 | "sha256:007dc055dbce5b1104876acee177dbfd18757e19d562cd440182e1f492e96b95", 53 | "sha256:031bf79a27d1c42f69c276d6221172417b47cb4b31cdc73d362a9bf5a1889b9f", 54 | "sha256:161d575fa49395860b75da5135162481768b11208490d5a2143ae6785123e77d", 55 | "sha256:24bbc3507fb6dfff663af7900a631f2aca90d5a445f272db5fc84999fa5718bc", 56 | "sha256:2efaeb1baff547063bad2b2893a8f5e9c459c4624e1a96644bbba08910ae34e0", 57 | "sha256:32200f562daaab472921a11cbb63780f1654552ae49518196fc361ed8e12e901", 58 | "sha256:3261fae28155e5c8634dd7710635fe540a05b58f160cef7713c7700cb9980e66", 59 | "sha256:3b54a9c68995ef4164567e2cd1a5e16db5dac30b2a50c39c82db8d4afaf14f63", 60 | "sha256:3c352ff634e289061711608f5e474ec38dbaa21e3e168820d53d5f4015e5b91b", 61 | "sha256:3fb47f97f1d338b943126e90b79cad50d4fcfa0b80637b5a9f468941dbbd9ce5", 62 | "sha256:441ce2a8c17683d97e06447fcbccbdb057cbf587c78eb75ae43ea7858042fe2c", 63 | "sha256:45535241baa0fc0ba2a43961a1ac7562ca3257f46c4c3e9c0de38b722be41bd1", 64 | "sha256:4aca81a687975b35e3e80bcf9aa93fe10cd57fac37bf18b2314c186095f57e05", 65 | "sha256:4cc563836f13c57f1473bc02d1e01fc37bab70ad4ee6be297d58c1d66bc819bf", 66 | "sha256:4fae0677f712ee090721d8b17f412f1cbceefbf0dc180fe91bab3232f38b4527", 67 | "sha256:58bc9fce3e1557d463ef5cee05391a05745fd95ed660f23c1742c711712c0abb", 68 | "sha256:664832fb88b8162268928df233f4b12a144a0c78b01d38b81bdcf0fc96668ecb", 69 | "sha256:70820a1c96311e02449591cbdf5cd1c6a34d5194d5b55094ab725364375c9eb2", 70 | "sha256:79b2ae94fa991be023832e6bcc00f41dbc8e5fe9d997a02db965831402551730", 71 | "sha256:83cf0228b2f694dcdba1374d5312f2277269d798e65f40344964f642935feac1", 72 | "sha256:87de598edfa2230ff274c4de7fcf24c73ffd96208c8e1912d5d0fee459767d75", 73 | "sha256:8f806bfd0f218477d7c46a11d3e52dc7f5fdfaa981b18202b7dc84bbc287463b", 74 | "sha256:90053234a6479738fd40d155268af631c7fca33365f964f2208867da1349294b", 75 | "sha256:a00dce2d96587651ef4fa192c17e039e8cfab63087c67e7d263a5533c7dad715", 76 | "sha256:a08cd07d3c3c17cd33d9e66ea9dee8f8fc1c48e2d11bd88fd2dc515a602c709b", 77 | "sha256:a19d39b02a24d3082856a5b06490b714a9d4179321225bbf22809ff1e1887cc8", 78 | "sha256:d00a669e4a5bec3ee6dbeeeedd82a405ced19f8aeefb109a012ea88a45afff96", 79 | "sha256:dab0c685f21f4a6c95bfc2afd1e7eae0033b403dd3d8c1b6d13a652ada75b348", 80 | "sha256:df561f65049ed3556e5b52541669310e88713fdae2934845ec3606f283337958", 81 | "sha256:e4570d16f88c7f3032ed909dc9e905a17da14a1c4cfd92608e3fda4cb1208bbd", 82 | "sha256:e77e4b983e2441aff0c0d07ee711110c106b625f440292dfe02a2f60c8218bd6", 83 | "sha256:e79212d09fc0e224d20b43ad44bb0a0a3416d1e04cf6b45fed265114a5d43d20", 84 | "sha256:f58b5ba13a5689ca8317b98439fccfbcc673acaaf8241c1869ceea40f5d585bf", 85 | "sha256:fef86115fdad7ae774720d7103aa776144cf9b66673b4afa9bcaa7af990ed07b" 86 | ], 87 | "markers": "python_version >= '3.6'", 88 | "version": "==2.0.0" 89 | }, 90 | "requests": { 91 | "hashes": [ 92 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", 93 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" 94 | ], 95 | "index": "pypi", 96 | "version": "==2.25.1" 97 | }, 98 | "urllib3": { 99 | "hashes": [ 100 | "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", 101 | "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" 102 | ], 103 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 104 | "version": "==1.26.4" 105 | } 106 | }, 107 | "develop": {} 108 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

𝘧𝘳𝘦𝘦𝘯𝘰𝘮-𝘱𝘺

3 | 4 | GitHub stars 5 | GitHub forks 6 | Telegram 7 | GitHub issues 8 | GitHub last commit 9 | 10 |
11 | 12 | ## 项目描述 🔑 13 | 14 | Freenom 自动续期域名的脚本 15 | 16 | ## 项目部署 🥳 17 | 18 | Python 运行环境 19 | 20 | * Windows、Linux、青龙、elecV2P 等 21 | 22 | ## 使用说明 🕹 23 | 24 |

🌏 通用版

25 | 26 | PC、VPS 等可直接运行, 无通知变量 27 | 28 | ```sh 29 | wget https://raw.githubusercontent.com/Oreomeow/freenom-py/main/FNplus.py 30 | ``` 31 | 32 | ```sh 33 | python3 FNplus.py -u USERNAME -p PASSWORD 34 | ``` 35 | 36 | `USERNAME` : Freenom 用户名 37 | `PASSWORD` : Freenom 密码 38 | 39 |

🐉 青龙邮件版 📧

40 | 41 | 1. 修改配置文件 42 | 43 | ```sh 44 | 45 | ## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可 46 | 47 | RepoFileExtensions="js py ts html" 48 | ``` 49 | 50 | 2. 添加定时拉取任务并运行 51 | 52 | ```sh 53 | ql repo https://github.com/Oreomeow/freenom-py.git "FN_extend.py" "" "utils|templates" 54 | ``` 55 | 56 | 3. 安装依赖 57 | 58 | ```sh 59 | docker exec -it qinglong bash # 进入容器内 60 | ``` 61 | 62 | ```sh 63 | cd /ql/scripts 64 | wget https://raw.githubusercontent.com/Oreomeow/freenom-py/main/requirements.txt -O requirements.txt 65 | pip3 install -r requirements.txt 66 | ``` 67 | 68 | 4. 添加环境变量 [示例](https://github.com/Oreomeow/freenom-py/issues/1#issuecomment-903344952) 69 | 70 | * 可看[脚本注释](https://raw.githubusercontent.com/Oreomeow/freenom-py/main/FN_extend.py) 71 | * 参考[下方表格](https://github.com/Oreomeow/freenom-py#%E9%82%AE%E4%BB%B6%E7%89%88-) 72 | 73 | 5. 运行一次 `FN_extend.py` 测试 74 | 75 |

🪁 elecV2P 邮件版 📧

76 | 77 | TASK -> 添加单个任务 -> 修改名称、时间、任务 -> JSMANAGE -> store/cookie 常量储存管理填写[环境变量](https://github.com/Oreomeow/freenom-py#%E9%82%AE%E4%BB%B6%E7%89%88-) 78 | 79 | 名称: Freenom 续期 80 | 81 | 时间: cron 定时 `25 7 */10 * *` 82 | 83 | 任务: 84 | 85 | ```url 86 | https://raw.githubusercontent.com/Oreomeow/freenom-py/main/FN_extend.js 87 | ``` 88 | 89 |

🐉 青龙消息版 📱

90 | 91 | 1. 面板添加定时任务,定时随意,运行 92 | 93 | ```sh 94 | ql raw https://raw.githubusercontent.com/Oreomeow/freenom-py/main/FNplus.py 95 | ``` 96 | 97 | 2. 添加[环境变量](https://github.com/Oreomeow/freenom-py#%E6%B6%88%E6%81%AF%E7%89%88-) [示例](https://github.com/Oreomeow/freenom-py/issues/1#issuecomment-903344952) 98 | 99 | 3. 运行一次 `FNplus.py` 测试 100 | 101 |

🪁 elecV2P 消息版 📱

102 | 103 | TASK -> 添加单个任务 -> 修改名称、时间、任务 -> JSMANAGE -> store/cookie 常量储存管理填写[环境变量](https://github.com/Oreomeow/freenom-py#%E6%B6%88%E6%81%AF%E7%89%88-) 104 | 105 | 名称: Freenom 续期 106 | 107 | 时间: cron 定时 `25 7 */10 * *` 108 | 109 | 任务: 110 | 111 | ```url 112 | https://raw.githubusercontent.com/Oreomeow/freenom-py/main/FNplus.js 113 | ``` 114 | 115 | ## 环境变量 🍒 116 | 117 | ### 邮件版 📧 118 | 119 | | 变量 / key | 描述 | 示例 / value | 120 | | ------------ | ------------------------ | ----------------------------------- | 121 | | FN_ID | Freenom 用户名 | 1234567890@gmail.com | 122 | | FN_PW | Freenom 密码 | 12345678 | 123 | | MAIL_USER | 发件人邮箱用户名 | address@vip.qq.com 或 123456@qq.com | 124 | | MAIL_ADDRESS | 发件人邮箱地址 | address@vip.qq.com 或 123456@qq.com | 125 | | MAIL_PW | 发件人邮箱授权码 | xxxxxxxxxxxxxxxx 看下方链接 | 126 | | MAIL_HOST | 发件人邮箱服务器 | smtp.qq.com 不填默认为这个 | 127 | | MAIL_PORT | 邮箱服务器端口 | 465 不填默认为这个 | 128 | | MAIL_TO | 收件人邮箱可与发件人相同 | address@vip.qq.com 或 123456@qq.com | 129 | 130 | * 填写总参考 131 | 132 | > [如何设置POP3/SMTP的SSL加密方式?](https://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=369) 133 | 134 | ### 消息版 📱 135 | 136 | | 变量 / key | 描述 | 参考 / value | 137 | | ------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 138 | | FN_ID | Freenom 用户名, 多账号空格隔开 | 1234567890@gmail.com 9876543210@enayu.com | 139 | | FN_PW | Freenom 密码, 多账号空格隔开 | 12345678 87654321 | 140 | | BARK | bark 设备码 | BARK 推送[使用](https://github.com/Sitoi/dailycheckin/issues/29), 填写 URL 即可, 例如: `https://api.day.app/DxHcxxxxxRxxxxxxcm/` | 141 | | PUSH_KEY | Server 酱 | server 酱推送[官方文档](https://sc.ftqq.com/3.version), 填写 `SCKEY` 代码即可 | 142 | | TG_BOT_TOKEN | tg 机器人 | 申请 [@BotFather](https://t.me/BotFather) 的 Token, 如 `10xxx4:AAFcqxxxxgER5uw` | 143 | | TG_USER_ID | tg 机器人 | 给 [@getidsbot](https://t.me/getidsbot) 发送 /start 获取到的纯数字 ID, 如 `1434078534` | 144 | | TG_PROXY_IP | * tg 机器人代理 IP 地址 | 代理类型为 http, 比如您代理是 `http://127.0.0.1:1080` , 则填写 `127.0.0.1` , 有密码例子: `username:password@127.0.0.1` | 145 | | TG_PROXY_PORT | * tg 机器人代理端口 | 代理端口号, 代理类型为 http, 比如您代理是 `http://127.0.0.1:1080` , 则填写 `1080` | 146 | | DD_BOT_TOKEN | 钉钉机器人 | 钉钉推送[官方文档](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq), 只需 `https://oapi.dingtalk.com/robot/send?access_token=XXX` 等于符号后面的 `XXX` | 147 | | DD_BOT_SECRET | 钉钉机器人 | 钉钉推送[官方文档](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq)密钥, 机器人安全设置页面, 加签一栏下面显示的 `SEC` 开头的字符串, 注: 填写了 `DD_BOT_TOKEN` 和 `DD_BOT_SECRET` , 钉钉机器人安全设置只需勾选加签即可, 其他选项不要勾选 | 148 | | QYWX_AM | 企业微信应用 | [参考文档](http://note.youdao.com/s/HMiudGkb), 依次填入 corpid, corpsecret, touser(注: 多个成员ID使用 \| 隔开), agentid, media_id(选填, 不填默认文本消息类型) | 149 | 150 | *\* 表示选填* 151 | 152 | * 调用模块 153 | 154 | > [notify.py](https://raw.githubusercontent.com/whyour/qinglong/master/sample/notify.py) 155 | 156 | ## 查看通知 📮 157 | 158 | 不出意外会收到一封关于域名续期的邮件或者 tg 等通知消息 159 | 160 | ## 写在最后 🔚 161 | 162 | 核心代码见 `utils/freenom.py` 163 | 164 | 此项目核心接口参考 [Freenom-PHP](https://github.com/luolongfei/freenom) 165 | 166 | **感谢不限于以下开发者** 167 | 168 | [@𝘴𝘩𝘶𝘢𝘪93](https://github.com/shuai93) 169 | 170 | [@𝘭𝘶𝘰𝘭𝘰𝘯𝘨𝘧𝘦𝘪](https://github.com/luolongfei) 171 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2020.12.5 2 | chardet==4.0.0 3 | idna==2.10 4 | Jinja2==3.0.0 5 | MarkupSafe==2.0.0 6 | requests==2.25.1 7 | urllib3==1.26.4 8 | -------------------------------------------------------------------------------- /templates/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Transactional Email 7 | 348 | 349 | 350 | 351 | 352 | 353 | 363 | 364 | 365 | 366 | 433 | 434 | 435 |
  367 |
368 |

账户 {{ payload.get('user') }} 今天所有域名续期情况如下:

369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 398 | 399 | 400 | 401 |
377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | {% for data in payload.get('results') %} 389 | 390 | 391 | 392 | 393 | 394 | 395 | {% endfor %} 396 |
域名剩余天数结果详情
{{data[0]}}{{data[1]}}{{data[3]}}查看详情
397 |
402 | 403 | 404 | 428 | 429 | 430 | 431 |
432 |
 
436 | 437 | 438 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oreomeow/freenom-py/5fcd6aab443a94b7a83b5350ece3c33d06d8b3fe/utils/__init__.py -------------------------------------------------------------------------------- /utils/exception.py: -------------------------------------------------------------------------------- 1 | class CustomException(Exception): 2 | def __init__(self, message: str): 3 | super().__init__(self) 4 | self.message: str = message 5 | 6 | def __str__(self): 7 | return self.message 8 | -------------------------------------------------------------------------------- /utils/freenom.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | 4 | import requests 5 | 6 | from utils import settings 7 | from utils.exception import CustomException 8 | 9 | 10 | class FreeNom: 11 | """FreeNom api 请求""" 12 | 13 | # 登录 14 | LOGIN_URL = "https://my.freenom.com/dologin.php" 15 | # 查看域名状态 16 | DOMAIN_STATUS_URL = "https://my.freenom.com/domains.php?a=renewals" 17 | # 域名续期 18 | RENEW_DOMAIN_URL = "https://my.freenom.com/domains.php?submitrenewals=true" 19 | 20 | TOKEN_REGEX = 'name="token"\svalue="(?P[a-z||A-Z||0-9]+)"' 21 | DOMAIN_INFO_REGEX = ( 22 | '(?P[^<]+)<\/td>[^<]+<\/td>[^<]+(?P\d+)[' 23 | '^&]+&domain=(?P\d+)"' 24 | ) 25 | LOGIN_STATUS_REGEX = "" 26 | 27 | def __init__(self): 28 | self.headers = { 29 | "Content-Type": "application/x-www-form-urlencoded", 30 | } 31 | self.session = requests.session() 32 | self.token_pattern = re.compile(self.TOKEN_REGEX) 33 | self.domain_info_pattern = re.compile(self.DOMAIN_INFO_REGEX) 34 | self.login_pattern = re.compile(self.LOGIN_STATUS_REGEX) 35 | 36 | def run(self) -> list: 37 | self.login() 38 | html = self.get_domains() 39 | token_match = self.token_pattern.findall(html) 40 | domain_info_match = self.domain_info_pattern.findall(html) 41 | login_match = self.login_pattern.findall(html) 42 | 43 | if not login_match: 44 | print("FreeNom login parse failed") 45 | raise CustomException("登录检查失败") 46 | 47 | if not token_match: 48 | print("FreeNom token parse failed") 49 | raise CustomException("页面token检查失败") 50 | 51 | if not domain_info_match: 52 | print("FreeNom domain info parse failed") 53 | raise CustomException("页面没有获取到域名信息") 54 | 55 | token = token_match[0] 56 | print(f"waiting for renew domain info is {domain_info_match}") 57 | 58 | result = [] 59 | 60 | for info in domain_info_match: 61 | time.sleep(1) 62 | domain, days, domain_id = info 63 | msg = "失败" 64 | 65 | if int(days) > 14: 66 | print( 67 | f"FreeNom domain {domain} can not renew, days until expiry is {days}" 68 | ) 69 | 70 | else: 71 | response = self.renew_domain(token, domain_id) 72 | 73 | if response.find("Order Confirmation") != -1: 74 | msg = "成功" 75 | print(f"FreeNom renew domain {domain} is success") 76 | 77 | result.append((domain, days, domain_id, msg)) 78 | return result 79 | 80 | def login(self) -> bool: 81 | data = {"username": settings.FN_ID, "password": settings.FN_PW} 82 | headers = {**self.headers, "Referer": "https://my.freenom.com/clientarea.php"} 83 | response = self.session.post(self.LOGIN_URL, data=data, headers=headers) 84 | 85 | if response.status_code == 200: 86 | return True 87 | print("FreeNom login failed") 88 | raise CustomException("调用登录接口失败") 89 | 90 | def get_domains(self) -> str: 91 | headers = {"Referer": "https://my.freenom.com/clientarea.php"} 92 | response = self.session.get(self.DOMAIN_STATUS_URL, headers=headers) 93 | 94 | if response.status_code == 200: 95 | return response.text 96 | print("FreeNom check domain status failed") 97 | raise CustomException("调用获取域名信息接口失败") 98 | 99 | def renew_domain(self, token, renewalid: str) -> str: 100 | headers = { 101 | **self.headers, 102 | "Referer": "https://my.freenom.com/domains.php?a=renewdomain&domain=" 103 | + "renewalid", 104 | } 105 | data = { 106 | "token": token, 107 | "renewalid": renewalid, 108 | f"renewalperiod[{renewalid}]": "12M", 109 | "paymentmethod": "credit", 110 | } 111 | 112 | response = self.session.post(self.RENEW_DOMAIN_URL, data=data, headers=headers) 113 | if response.status_code == 200: 114 | return response.text 115 | print("FreeNom renew domain failed") 116 | raise CustomException("调用续期接口失败接口失败") 117 | 118 | def __del__(self): 119 | self.session.close() 120 | -------------------------------------------------------------------------------- /utils/mail.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | import traceback 3 | from email.mime.multipart import MIMEMultipart 4 | from email.mime.text import MIMEText 5 | 6 | from jinja2 import Environment, FileSystemLoader, Template 7 | 8 | from utils import settings 9 | 10 | 11 | class EmailPoster: 12 | """邮件发送基础类""" 13 | 14 | @staticmethod 15 | def get_template() -> Template: 16 | loader = FileSystemLoader("templates") 17 | env = Environment(autoescape=True, loader=loader) 18 | return env.get_template("default.html") 19 | 20 | def send(self, data: dict): 21 | payload = data.get("payload", {}) 22 | if payload: 23 | template = self.get_template() 24 | content = template.render(payload=payload) 25 | else: 26 | content = data.get("body", "") 27 | subject = data.get("subject", "") 28 | mail_to = data.get("to", []) 29 | mail_from = data.get("from", settings.MAIL_ADDRESS) 30 | self._send(content, subject, mail_from, mail_to) 31 | 32 | @staticmethod 33 | def _send(content: str, subject: str, mail_from: str, mail_to: list): 34 | msg_root = MIMEMultipart("related") 35 | msg_text = MIMEText(content, "html", "utf-8") 36 | msg_root.attach(msg_text) 37 | msg_root["Subject"] = subject 38 | msg_root["From"] = mail_from 39 | msg_root["To"] = ";".join(mail_to) 40 | 41 | try: 42 | smtp = smtplib.SMTP_SSL(settings.MAIL_HOST, settings.MAIL_PORT) 43 | # smtp.set_debuglevel(1) 44 | smtp.ehlo() 45 | smtp.login(settings.MAIL_USER, settings.MAIL_PW) 46 | smtp.sendmail(settings.MAIL_ADDRESS, mail_to, msg_root.as_string()) 47 | smtp.quit() 48 | except Exception: 49 | print(traceback.format_exc()) 50 | -------------------------------------------------------------------------------- /utils/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # qq mail 4 | MAIL_ADDRESS = str(os.getenv("MAIL_ADDRESS", "")) 5 | MAIL_HOST = str(os.getenv("SMTP_HOST", "smtp.qq.com")) 6 | MAIL_PW = str(os.getenv("MAIL_PW", "")) 7 | MAIL_PORT = int(os.getenv("SMTP_PORT", 465)) 8 | MAIL_TO = str(os.getenv("MAIL_TO", "")) 9 | MAIL_USER = str(os.getenv("MAIL_USER", "")) 10 | 11 | 12 | # free nom 13 | FN_ID = str(os.getenv("FN_ID", "")) 14 | FN_PW = str(os.getenv("FN_PW", "")) 15 | --------------------------------------------------------------------------------