🚂🔨FN_extend

This commit is contained in:
Leon 2021-08-11 18:33:18 +08:00
parent 596130b3a8
commit 5795e02548
9 changed files with 607 additions and 1 deletions

View 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()

View 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

View 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>&nbsp;</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>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

View File

@ -0,0 +1,9 @@
class CustomException(Exception):
def __init__(self, message):
super().__init__(self)
self.message = message
def __str__(self):
return self.message

View 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()

View 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))

View 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", "")

View File

@ -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]