🚂🔨FN_extend
This commit is contained in:
parent
596130b3a8
commit
5795e02548
53
Scripts/py/FreeNom/FN_extend.py
Normal file
53
Scripts/py/FreeNom/FN_extend.py
Normal file
@ -0,0 +1,53 @@
|
||||
# -*- coding: utf8 -*-
|
||||
'''
|
||||
Author: shuai93
|
||||
Modifier: Oreo
|
||||
Date: Wed Aug 11 10:15:41 UTC 2021
|
||||
建议cron: 25 7 */10 * * python3 FN_extend.py
|
||||
------------
|
||||
环境变量说明 示例
|
||||
FN_ID: Freenom 用户名 1234567890@gmail.com
|
||||
FN_PW: Freenom 密码 12345678
|
||||
MAIL_USER: 发件人邮箱用户名 address@vip.qq.com 或 123456@qq.com
|
||||
MAIL_ADDRESS: 发件人邮箱地址 address@vip.qq.com 或 123456@qq.com
|
||||
* MAIL_HOST: 发件人邮箱服务器 smt.qq.com 不填默认为这个
|
||||
* MAIL_PORT: 邮箱服务器端口 465 不填默认为这个
|
||||
MAIL_TO: 收件人邮箱可与发件人相同 address@vip.qq.com 或 123456@qq.com
|
||||
|
||||
填写总参考:https://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=369
|
||||
------------
|
||||
依赖模块说明
|
||||
pip install -r requirements.txt / pip3 install -r requirements.txt
|
||||
'''
|
||||
from utils.settings import *
|
||||
from utils.exception import CustomException
|
||||
from utils.freenom import FreeNom
|
||||
from utils.mail import EmailPoster
|
||||
|
||||
|
||||
def main():
|
||||
print("配置信息")
|
||||
print([MAIL_TO, MAIL_PORT, MAIL_HOST, MAIL_ADDRESS, MAIL_PW, MAIL_USER, FN_ID, FN_PW])
|
||||
if not all([MAIL_TO, MAIL_PORT, MAIL_HOST, MAIL_ADDRESS, MAIL_PW, MAIL_USER, FN_ID, FN_PW]):
|
||||
raise CustomException("参数缺失")
|
||||
|
||||
to = [MAIL_TO]
|
||||
|
||||
body = {
|
||||
'subject': "FreeNom 自动续期",
|
||||
'to': to,
|
||||
}
|
||||
try:
|
||||
results = FreeNom().run()
|
||||
body['payload'] = {
|
||||
"results": results,
|
||||
"user": FN_ID
|
||||
}
|
||||
except CustomException as e:
|
||||
body['body'] = e.message
|
||||
|
||||
EmailPoster().send(data=body)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
Scripts/py/FreeNom/requirements.txt
Normal file
7
Scripts/py/FreeNom/requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
certifi==2020.12.5
|
||||
chardet==4.0.0
|
||||
idna==2.10
|
||||
Jinja2==3.0.0
|
||||
MarkupSafe==2.0.0
|
||||
requests==2.25.1
|
||||
urllib3==1.26.4
|
||||
344
Scripts/py/FreeNom/templates/default.html
Normal file
344
Scripts/py/FreeNom/templates/default.html
Normal file
@ -0,0 +1,344 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Simple Transactional Email</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%; }
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%; }
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%; }
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top; }
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%; }
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
Margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px; }
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
Margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px; }
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%; }
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px; }
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
Margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%; }
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center; }
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
Margin-bottom: 30px; }
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize; }
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
Margin-bottom: 15px; }
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px; }
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: underline; }
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%; }
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 15px; }
|
||||
.btn table {
|
||||
width: auto; }
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center; }
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize; }
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db; }
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff; }
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0; }
|
||||
|
||||
.first {
|
||||
margin-top: 0; }
|
||||
|
||||
.align-center {
|
||||
text-align: center; }
|
||||
|
||||
.align-right {
|
||||
text-align: right; }
|
||||
|
||||
.align-left {
|
||||
text-align: left; }
|
||||
|
||||
.clear {
|
||||
clear: both; }
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0; }
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0; }
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0; }
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none; }
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
Margin: 20px 0; }
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important; }
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important; }
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important; }
|
||||
table[class=body] .content {
|
||||
padding: 0 !important; }
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important; }
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important; }
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important; }
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important; }
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important; }}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%; }
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%; }
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important; }
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important; }
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important; } }
|
||||
td {
|
||||
|
||||
text-align:center;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content" style="text-align:center">
|
||||
<h4 style="text-align:left" >账户 <span style="color: #34495e"> {{ payload.get('user') }} </span> 今天所有域名续期情况如下:</h4>
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<table class="main">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table style="width: 100%;margin:auto" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>域名</th>
|
||||
<th>剩余天数</th>
|
||||
<th>结果</th>
|
||||
<th>详情</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{% for data in payload.get('results') %}
|
||||
<tr>
|
||||
<td>{{data[0]}}</td>
|
||||
<td>{{data[1]}}</td>
|
||||
<td>{{data[3]}}</td>
|
||||
<td><a href="https://my.freenom.com/domains.php?a=renewdomain&domain={{data[2]}}" >查看详情</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<span class="apple-link"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block powered-by">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
0
Scripts/py/FreeNom/utils/__init__.py
Normal file
0
Scripts/py/FreeNom/utils/__init__.py
Normal file
9
Scripts/py/FreeNom/utils/exception.py
Normal file
9
Scripts/py/FreeNom/utils/exception.py
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
class CustomException(Exception):
|
||||
|
||||
def __init__(self, message):
|
||||
super().__init__(self)
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
126
Scripts/py/FreeNom/utils/freenom.py
Normal file
126
Scripts/py/FreeNom/utils/freenom.py
Normal file
@ -0,0 +1,126 @@
|
||||
import re
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from utils import settings
|
||||
from utils.exception import CustomException
|
||||
|
||||
|
||||
class FreeNom(object):
|
||||
"""
|
||||
FreeNom api请求
|
||||
"""
|
||||
# 登录
|
||||
LOGIN_URL = 'https://my.freenom.com/dologin.php'
|
||||
# 查看域名状态
|
||||
DOMAIN_STATUS_URL = 'https://my.freenom.com/domains.php?a=renewals'
|
||||
# 域名续期
|
||||
RENEW_DOMAIN_URL = 'https://my.freenom.com/domains.php?submitrenewals=true'
|
||||
|
||||
TOKEN_REGEX = 'name="token"\svalue="(?P<token>[a-z||A-Z||0-9]+)"'
|
||||
DOMAIN_INFO_REGEX = '<tr><td>(?P<domain>[^<]+)<\/td><td>[^<]+<\/td><td>[^<]+<span class="[^"]+">(?P<days>\d+)[' \
|
||||
'^&]+&domain=(?P<id>\d+)"'
|
||||
LOGIN_STATUS_REGEX = '<li.*?Logout.*?<\/li>'
|
||||
|
||||
def __init__(self):
|
||||
self.headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
self.session = requests.session()
|
||||
self.token_pattern = re.compile(self.TOKEN_REGEX)
|
||||
self.domain_info_pattern = re.compile(self.DOMAIN_INFO_REGEX)
|
||||
self.login_pattern = re.compile(self.LOGIN_STATUS_REGEX)
|
||||
|
||||
def run(self) -> list:
|
||||
self.login()
|
||||
html = self.get_domains()
|
||||
token_match = self.token_pattern.findall(html)
|
||||
domain_info_match = self.domain_info_pattern.findall(html)
|
||||
login_match = self.login_pattern.findall(html)
|
||||
|
||||
if not login_match:
|
||||
print("FreeNom login parse failed")
|
||||
raise CustomException("登录检查失败")
|
||||
|
||||
if not token_match:
|
||||
print("FreeNom token parse failed")
|
||||
raise CustomException("页面token检查失败")
|
||||
|
||||
if not domain_info_match:
|
||||
print("FreeNom domain info parse failed")
|
||||
raise CustomException("页面没有获取到域名信息")
|
||||
|
||||
token = token_match[0]
|
||||
print(f"waiting for renew domain info is {domain_info_match}")
|
||||
|
||||
result = []
|
||||
|
||||
for info in domain_info_match:
|
||||
time.sleep(1)
|
||||
domain, days, domain_id = info
|
||||
msg = "失败"
|
||||
|
||||
if int(days) > 14:
|
||||
print(f"FreeNom domain {domain} can not renew, days until expiry is {days}")
|
||||
|
||||
else:
|
||||
response = self.renew_domain(token, domain_id)
|
||||
|
||||
if response.find("Order Confirmation") != -1:
|
||||
msg = "成功"
|
||||
print(f"FreeNom renew domain {domain} is success")
|
||||
|
||||
result.append((domain, days, domain_id, msg))
|
||||
return result
|
||||
|
||||
def login(self) -> bool:
|
||||
data = {
|
||||
'username': settings.FN_ID,
|
||||
'password': settings.FN_PW
|
||||
}
|
||||
headers = {
|
||||
**self.headers,
|
||||
'Referer': 'https://my.freenom.com/clientarea.php'
|
||||
}
|
||||
response = self.session.post(self.LOGIN_URL, data=data, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
print("FreeNom login failed")
|
||||
raise CustomException("调用登录接口失败")
|
||||
|
||||
def get_domains(self) -> str:
|
||||
headers = {
|
||||
'Referer': 'https://my.freenom.com/clientarea.php'
|
||||
}
|
||||
response = self.session.get(self.DOMAIN_STATUS_URL, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.text
|
||||
else:
|
||||
print("FreeNom check domain status failed")
|
||||
raise CustomException("调用获取域名信息接口失败")
|
||||
|
||||
def renew_domain(self, token, renewalid) -> str:
|
||||
headers = {
|
||||
**self.headers,
|
||||
"Referer": "https://my.freenom.com/domains.php?a=renewdomain&domain=" + "renewalid"
|
||||
}
|
||||
data = {
|
||||
"token": token,
|
||||
"renewalid": renewalid,
|
||||
f"renewalperiod[{renewalid}]": "12M",
|
||||
'paymentmethod': 'credit'
|
||||
}
|
||||
|
||||
response = self.session.post(self.RENEW_DOMAIN_URL, data=data, headers=headers)
|
||||
if response.status_code == 200:
|
||||
return response.text
|
||||
else:
|
||||
print("FreeNom renew domain failed")
|
||||
raise CustomException("调用续期接口失败接口失败")
|
||||
|
||||
def __del__(self):
|
||||
self.session.close()
|
||||
52
Scripts/py/FreeNom/utils/mail.py
Normal file
52
Scripts/py/FreeNom/utils/mail.py
Normal file
@ -0,0 +1,52 @@
|
||||
import smtplib
|
||||
import traceback
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from jinja2 import FileSystemLoader, Environment, Template
|
||||
|
||||
from . import settings
|
||||
|
||||
|
||||
class EmailPoster(object):
|
||||
"""
|
||||
邮件发送基础类
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_template():
|
||||
loader = FileSystemLoader('templates')
|
||||
env = Environment(loader=loader)
|
||||
template = env.get_template("default.html")
|
||||
return template
|
||||
|
||||
def send(self, data: dict):
|
||||
payload = data.get("payload", {})
|
||||
if payload:
|
||||
template = self.get_template()
|
||||
content = template.render(payload=payload)
|
||||
else:
|
||||
content = data.get('body', '')
|
||||
subject = data.get('subject', '')
|
||||
mail_to = data.get('to', [])
|
||||
mail_from = data.get('from', settings.MAIL_ADDRESS)
|
||||
self._send(content, subject, mail_from, mail_to)
|
||||
|
||||
@staticmethod
|
||||
def _send(content: str, subject: str, mail_from: str, mail_to: list):
|
||||
msg_root = MIMEMultipart('related')
|
||||
msg_text = MIMEText(content, 'html', 'utf-8')
|
||||
msg_root.attach(msg_text)
|
||||
msg_root['Subject'] = subject
|
||||
msg_root['From'] = mail_from
|
||||
msg_root['To'] = ";".join(mail_to)
|
||||
|
||||
try:
|
||||
smtp = smtplib.SMTP_SSL(settings.MAIL_HOST, settings.MAIL_PORT)
|
||||
# smtp.set_debuglevel(1)
|
||||
smtp.ehlo()
|
||||
smtp.login(settings.MAIL_USER, settings.MAIL_PW)
|
||||
smtp.sendmail(settings.MAIL_ADDRESS, mail_to, msg_root.as_string())
|
||||
smtp.quit()
|
||||
except Exception as e:
|
||||
print(traceback.format_exc(e))
|
||||
15
Scripts/py/FreeNom/utils/settings.py
Normal file
15
Scripts/py/FreeNom/utils/settings.py
Normal file
@ -0,0 +1,15 @@
|
||||
import os
|
||||
|
||||
# qq mail
|
||||
MAIL_ADDRESS = os.getenv("MAIL_ADDRESS", "")
|
||||
MAIL_HOST = os.getenv("SMTP_HOST", "smtp.qq.com")
|
||||
MAIL_PW= os.getenv("MAIL_PW", "")
|
||||
MAIL_PORT = int(os.getenv("SMTP_PORT", 465))
|
||||
MAIL_TO = os.getenv("MAIL_TO", "")
|
||||
MAIL_USER = os.getenv("MAIL_USER", "")
|
||||
|
||||
|
||||
# free nom
|
||||
FN_ID = os.getenv("FN_ID", "")
|
||||
FN_PW = os.getenv("FN_PW", "")
|
||||
|
||||
@ -11,7 +11,7 @@ Date: Tue Aug 10 08:24:30 UTC 2021
|
||||
MI_USER: 账号 仅支持手机号,多账号用 # 分隔
|
||||
MI_PWD: 密码 多账号用 # 分隔,且与账号一一对应
|
||||
STEP: 步数 空或不填则为 18000-25000 之间随机,自定义示例: 18763 或 19000-24000
|
||||
PMODE: 推送模式 || PKEY: 具体推送格式填写(不带 [ ],请用具体的值代替)
|
||||
PMODE: 推送模式 || PKEY: 具体推送格式填写(不带 [TG: ],请用具体的值代替)
|
||||
wx [Server 酱: skey]
|
||||
nwx [新 Server 酱: skey]
|
||||
tg [TG: tg_bot_token@user_id]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user