├── .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 |

5 |

6 |

7 |

8 |

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 |
367 |
368 | 账户 {{ payload.get('user') }} 今天所有域名续期情况如下:
369 |
370 |
371 |
372 |
373 | |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 | 域名 |
382 | 剩余天数 |
383 | 结果 |
384 | 详情 |
385 |
386 |
387 |
388 | {% for data in payload.get('results') %}
389 |
390 | {{data[0]}} |
391 | {{data[1]}} |
392 | {{data[3]}} |
393 | 查看详情 |
394 |
395 | {% endfor %}
396 |
397 | |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
428 |
429 |
430 |
431 |
432 | |
433 | |
434 |
435 |
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 |
--------------------------------------------------------------------------------
| |