├── README.md
├── eu.py
└── gmail_api.py
/README.md:
--------------------------------------------------------------------------------
1 | # 原作者:[@SAOJSM](https://github.com/SAOJSM/EU_CHICK_EXTEND_CHT) Salute!
2 | 1.本文修改自SAOJSM,作流程说明,简单修改,个人备份,Star请给原作者
3 | 2.仅支持TGBOT通知
4 | 3.仅支持Gmail邮箱
5 |
6 | # 部署流程
7 |
8 | ## 一、注册[TrueCaptcha](https://truecaptcha.org/)
9 | 前往[页面](https://truecaptcha.org/profile.html)获取`userid`和`apikey`(每日免费100次)
10 |
11 | ## 二、TG机器人注册
12 | 按照[小白教程](https://www.haianet.cn/334.html)获取`Token`和`Id`
13 |
14 | ## 三、获取Gmail Api Token
15 | ### 步骤 1
16 | #### 1-1 使用德鸡账户登陆[Google Cloud](https://console.cloud.google.com/)
17 | #### 1-2 左上角三条横`导航菜单`-`API和服务`-`已启用的 API 和服务`-`创建项目`,项目名随意
18 | #### 1-3 `已启用的 API 和服务`-`刚创建的项目`-`启用API和服务`-搜索`Gmail API`并启用
19 | #### 1-4 `已启用的 API 和服务`-`OAuth 同意屏幕`-`创建`-用户名称输入:`GMAIL VERIFY`-填写用户支持电子邮件和开发者联系电子邮件地址为`登陆的邮箱`-`保存并继续`-`保存并继续`-测试用户+ADD USERS 输入`登陆的邮箱`添加-`保存并继续`
20 | #### 1-5 `已启用的 API 和服务`-`凭据-创建凭据-OAuth 客户端 ID-Web 应用`-添加已获授权的重定向 URI`http://localhost:36666/`-创建
21 | #### 1-6 下载JSON,改名为credentials.json
22 |
23 | ### 步骤 2
24 | #### 2-1 打包仓库文件到win本地,或点击[蓝奏云](https://wwk.lanzoul.com/b01k3s80b)(密码5sq5)下载。
25 | #### 2-2 win配置python环境,上面`蓝奏云`提供python 3.10.7安装包。([小白教程](https://blog.csdn.net/weixin_43831559/article/details/121911854))
26 |
27 |
28 | 
29 | 配置需注意上图前后顺序,路径最好全英文
30 |
31 |
32 | #### 2-3 解压下载的包文件,将`1-6`获取的credentials.json放入包文件目录。这里假设包文件目录为
33 | `E:\Users\Administrator\Desktop\script\euserv` //请务必在后面的命令中修改成你自己的
34 | #### 2-4 cmd输入以下命令(需科学上网)
35 | ```
36 | pip install google-api-python-client
37 | pip install google_auth_oauthlib
38 | pip install socks
39 | pip install -U requests[socks]
40 | ```
41 | ```
42 | py E:\Users\Administrator\Desktop\script\euserv\gmail_api.py 你的邮箱用户名
43 | ```
44 | #### 2-5 输入完以上命令浏览器会自动跳转,登陆后一直点`继续`完成授权。
45 | `The authentication flow has completed. You may close this window.`表示授权完成。
46 | #### 2-6 包文件中会自动生成一个文件"token_你的email.json"
47 |
48 | ## 部署至vps
49 | ### 打开vps,root目录下新建euserv文件夹
50 | #### 将`credentials.json`、`eu.py`、'gmail_api.py'、`token_你的email.json`放入vps euserv文件夹
51 | ```
52 | wget https://raw.githubusercontent.com/Zpipishrimp/EU/main/gmail_api.py
53 | wget https://raw.githubusercontent.com/Zpipishrimp/EU/main/eu.py
54 | ```
55 |
56 | ### 修改 eu.py(26-30行)
57 | ```
58 | TG_BOT_TOKEN = '你的TG_BOT_TOKEN'
59 | TG_USER_ID = '你的TG_USER_ID'
60 |
61 | USERNAME = os.environ.get("EUSERV_USERNAME", "德鸡用户名")
62 | PASSWORD = os.environ.get("EUSERV_PASSWORD", "德鸡密码")
63 |
64 | TRUECAPTCHA_USERID = os.environ.get("TRUECAPTCHA_USERID", "TrueCaptcha网站获取的userid")
65 | TRUECAPTCHA_APIKEY = os.environ.get("TRUECAPTCHA_APIKEY", "TrueCaptcha网站获取的apikey")
66 | ```
67 | ### 执行以下命令
68 | ```
69 | apt install python3-pip //安装python3,已装跳过
70 | pip3 install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib beautifulsoup4 requests pysocks
71 | pip install beautifulsoup4 //报错No module named 'bs4'时输入,否则跳过
72 | cd /root/euserv
73 | python3 eu.py
74 | 你可能需要执行两次python3 eu.py
75 | ```
76 | ###成功图
77 |
78 |
79 | 
80 | 
81 |
82 | ### 设置定时任务
83 |
84 | ```
85 | crontab -e
86 |
87 | 0,10 17 10 * * /usr/bin/python3 /root/euserv/eu.py
88 |
89 | crontab -l //查看定时是否成功
90 | ```
91 | ps:`0,10 17 10 * *`表示每月10号,17:00和17:10分执行脚本。请查看你的德鸡到期时间,修改定时。
92 | (!定时两次是为了确保续期成功,第一次运行脚本可能仅登陆成功而没有续期)
93 |
94 | # Token失效
95 | 失效TG推送:`Token has been expired or revoled.`
96 |
97 | 此时只需重复`py E:\Users\Administrator\Desktop\script\euserv\gmail_api.py 你的邮箱用户名`获取,替换`token_你的email.json`
98 |
--------------------------------------------------------------------------------
/eu.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 |
3 | import os
4 | import re
5 | import json
6 | import time
7 | import base64
8 |
9 | from email.mime.application import MIMEApplication
10 | from email.mime.multipart import MIMEMultipart
11 | from email.mime.text import MIMEText
12 | from smtplib import SMTP_SSL, SMTPDataError
13 |
14 | import requests
15 | from bs4 import BeautifulSoup
16 | from base64 import urlsafe_b64decode
17 | from gmail_api import *
18 |
19 | dir_name = os.path.dirname(os.path.abspath(__file__)) + os.sep
20 | os.chdir(dir_name)
21 |
22 | TG_BOT_TOKEN = '你的TG_BOT_TOKEN'
23 | TG_USER_ID = '你的TG_USER_ID'
24 | TG_API_HOST = 'api.telegram.org'
25 |
26 | USERNAME = os.environ.get("EUSERV_USERNAME", "德鸡用户名")
27 | PASSWORD = os.environ.get("EUSERV_PASSWORD", "德鸡密码")
28 |
29 | TRUECAPTCHA_USERID = os.environ.get("TRUECAPTCHA_USERID", "TrueCaptcha网站获取的userid")
30 | TRUECAPTCHA_APIKEY = os.environ.get("TRUECAPTCHA_APIKEY", "TrueCaptcha网站获取的apikey")
31 |
32 | PIN_KEY_WORD = 'EUserv'
33 |
34 | # Maximum number of login retry
35 | LOGIN_MAX_RETRY_COUNT = 10
36 |
37 |
38 | # options: True or False
39 | TRUECAPTCHA_CHECK_USAGE = True
40 |
41 |
42 | user_agent = (
43 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
44 | "Chrome/99.0.4844.51 Safari/537.36"
45 | )
46 | desp = "" # 空值
47 |
48 | unixTimeToDate = lambda t: time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t))
49 |
50 | def log(info: str):
51 | print(info)
52 | global desp
53 | desp = desp + info + "\n"
54 |
55 |
56 | def login_retry(*args, **kwargs):
57 | def wrapper(func):
58 | def inner(username, password):
59 | max_retry = kwargs.get("max_retry")
60 | # default retry 3 times
61 | if not max_retry:
62 | max_retry = 3
63 | number = 0
64 | while number < max_retry:
65 | try:
66 | number += 1
67 | if number > 1:
68 | log("[EUserv] Login tried the {}th time".format(number))
69 | sess_id, session = func(username, password)
70 | if sess_id != "-1":
71 | return sess_id, session
72 | else:
73 | if number == max_retry:
74 | return sess_id, session
75 | except BaseException as e:
76 | log(str(e))
77 | else:
78 | return None, None
79 | return inner
80 | return wrapper
81 |
82 |
83 | def captcha_solver(captcha_image_url: str, session: requests.session) -> dict:
84 | """
85 | TrueCaptcha API doc: https://apitruecaptcha.org/api
86 | Free to use 100 requests per day.
87 | """
88 | response = session.get(captcha_image_url)
89 | encoded_string = base64.b64encode(response.content).decode()
90 | url = "https://api.apitruecaptcha.org/one/gettext"
91 |
92 | data = {
93 | "userid": TRUECAPTCHA_USERID,
94 | "apikey": TRUECAPTCHA_APIKEY,
95 | "case": "mixed",
96 | "mode": "default", #(human | default)
97 | "data": encoded_string
98 | }
99 | r = requests.post(url=url, json=data)
100 | j = json.loads(r.text)
101 | return j
102 |
103 |
104 | def handle_captcha_solved_result(solved: dict) -> str:
105 | """Since CAPTCHA sometimes appears as a very simple binary arithmetic expression.
106 | But since recognition sometimes doesn't show the result of the calculation directly,
107 | that's what this function is for.
108 | """
109 | if "result" in solved:
110 | solved_text = str(solved["result"])
111 | if "RESULT IS" in solved_text:
112 | log("[Captcha Solver] You are using the demo apikey.")
113 | print("There is no guarantee that demo apikey will work in the future!")
114 | # because using demo apikey
115 | text = re.findall(r"RESULT IS . (.*) .", solved_text)[0]
116 | else:
117 | # using your own apikey
118 | log("[Captcha Solver] You are using your own apikey.")
119 | text = solved_text
120 | operators = ["X", "x", "+", "-"]
121 | if any(x in text for x in operators):
122 | for operator in operators:
123 | operator_pos = text.find(operator)
124 | if operator == "x" or operator == "X":
125 | operator = "*"
126 | if operator_pos != -1:
127 | left_part = text[:operator_pos]
128 | right_part = text[operator_pos + 1 :]
129 | if left_part.isdigit() and right_part.isdigit():
130 | return eval(
131 | "{left} {operator} {right}".format(
132 | left=left_part, operator=operator, right=right_part
133 | )
134 | )
135 | else:
136 | # Because these symbols("X", "x", "+", "-") do not appear at the same time,
137 | # it just contains an arithmetic symbol.
138 | return text
139 | else:
140 | return text
141 | else:
142 | print(solved)
143 | raise KeyError("Failed to find parsed results.")
144 |
145 |
146 | def get_captcha_solver_usage() -> dict:
147 | url = "https://api.apitruecaptcha.org/one/getusage"
148 |
149 | params = {
150 | "username": TRUECAPTCHA_USERID,
151 | "apikey": TRUECAPTCHA_APIKEY,
152 | }
153 | r = requests.get(url=url, params=params)
154 | j = json.loads(r.text)
155 | return j
156 |
157 |
158 | @login_retry(max_retry=LOGIN_MAX_RETRY_COUNT)
159 | def login(username: str, password: str) -> (str, requests.session):
160 | headers = {"user-agent": user_agent, "origin": "https://www.euserv.com"}
161 | url = "https://support.euserv.com/index.iphp"
162 | captcha_image_url = "https://support.euserv.com/securimage_show.php"
163 | session = requests.Session()
164 |
165 | sess = session.get(url, headers=headers)
166 | sess_id = re.findall("PHPSESSID=(\\w{10,100});", str(sess.headers))[0]
167 | # visit png
168 | logo_png_url = "https://support.euserv.com/pic/logo_small.png"
169 | session.get(logo_png_url, headers=headers)
170 |
171 | login_data = {
172 | "email": username,
173 | "password": password,
174 | "form_selected_language": "en",
175 | "Submit": "Login",
176 | "subaction": "login",
177 | "sess_id": sess_id,
178 | }
179 | r = session.post(url, headers=headers, data=login_data)
180 | r.raise_for_status()
181 |
182 | if (
183 | r.text.find("Hello") == -1
184 | and r.text.find("Confirm or change your customer data here") == -1
185 | ):
186 | if "To finish the login process please solve the following captcha." in r.text:
187 | log("[Captcha Solver] 进行验证码识别...")
188 | solved_result = captcha_solver(captcha_image_url, session)
189 | if not "result" in solved_result:
190 | print(solved_result)
191 | raise KeyError("Failed to find parsed results.")
192 | captcha_code = handle_captcha_solved_result(solved_result)
193 | log("[Captcha Solver] 识别的验证码是: {}".format(captcha_code))
194 |
195 | if TRUECAPTCHA_CHECK_USAGE:
196 | usage = get_captcha_solver_usage()
197 | log(
198 | "[Captcha Solver] current date {0} api usage count: {1}".format(
199 | usage[0]["date"], usage[0]["count"]
200 | )
201 | )
202 |
203 | r = session.post(
204 | url,
205 | headers=headers,
206 | data={
207 | "subaction": "login",
208 | "sess_id": sess_id,
209 | "captcha_code": captcha_code,
210 | },
211 | )
212 | if (
213 | r.text.find(
214 | "To finish the login process please solve the following captcha."
215 | )
216 | == -1
217 | ):
218 | log("[Captcha Solver] 验证通过")
219 | return sess_id, session
220 | else:
221 | log("[Captcha Solver] 验证失败")
222 | return "-1", session
223 |
224 | if 'To finish the login process enter the PIN that you receive via email' in r.text:
225 | request_time = time.time()
226 |
227 | c_id_re = re.search('c_id" value="(.*?)"', r.text)
228 | c_id = c_id_re.group(1) if c_id_re else None
229 | pin_code = wait_for_email(request_time)
230 |
231 | payload = {
232 | "pin": pin_code,
233 | "Submit": "Confirm",
234 | "subaction": "login",
235 | "sess_id": sess_id,
236 | "c_id": c_id,
237 | }
238 | r = session.post(url, headers=headers, data=payload)
239 | if 'Logout' in r.text and 'enter the PIN that you receive via email' not in r.text:
240 | return sess_id, session
241 | else:
242 | return "-1", session
243 | else:
244 | return sess_id, session
245 |
246 |
247 | def get_servers(sess_id: str, session: requests.session) -> {}:
248 | d = {}
249 | url = "https://support.euserv.com/index.iphp?sess_id=" + sess_id
250 | headers = {"user-agent": user_agent, "origin": "https://www.euserv.com"}
251 | r = session.get(url=url, headers=headers)
252 | r.raise_for_status()
253 | soup = BeautifulSoup(r.text, "html.parser")
254 | for tr in soup.select(
255 | "#kc2_order_customer_orders_tab_content_1 .kc2_order_table.kc2_content_table tr"
256 | ):
257 | server_id = tr.select(".td-z1-sp1-kc")
258 | if not len(server_id) == 1:
259 | continue
260 | flag = (
261 | True
262 | if tr.select(".td-z1-sp2-kc .kc2_order_action_container")[0]
263 | .get_text()
264 | .find("Contract extension possible from")
265 | == -1
266 | else False
267 | )
268 | d[server_id[0].get_text()] = flag
269 | return d
270 |
271 |
272 | def get_verification_code(service, email_id, request_time):
273 | email = service.users().messages().get(userId='me', id=email_id.get('id')).execute()
274 | internalDate = float(email.get("internalDate")) / 1000
275 |
276 | if internalDate > request_time-8:
277 | if email.get('payload').get('body').get('size'):
278 | data = urlsafe_b64decode(email.get('payload').get('body').get('data')).decode()
279 | else:
280 | part = email.get('payload').get("parts")[0]
281 | data = urlsafe_b64decode(part.get('body').get('data')).decode()
282 | pin_code_re = re.search('PIN:\s+(.+?)\s+', data)
283 | pin_code = pin_code_re.group(1) if pin_code_re else None
284 | return pin_code
285 |
286 | def wait_for_email(request_time):
287 | try:
288 | service = gmail_authenticate(userId=userId)
289 | # get emails that match the query you specify from the command lines
290 | while time.time() < request_time + 120: # wait 2 min
291 | results = search_messages(service, PIN_KEY_WORD)
292 | print('Email id search result:' , results)
293 | # for each email matched, read it (output plain/text to console & save HTML and attachments)
294 | if results:
295 | pin_code = get_verification_code(service, results[0], request_time)
296 | if pin_code:
297 | log('[Email] pin code:' + pin_code)
298 | return pin_code
299 | time.sleep(5)
300 | else:
301 | log('[Email] Did not receive the email in 2 minutes.')
302 | return False
303 | except BaseException as e:
304 | log('[Email] ' + str(e))
305 | return False
306 |
307 | def renew(
308 | sess_id: str, session: requests.session, password: str, order_id: str
309 | ) -> bool:
310 | url = "https://support.euserv.com/index.iphp"
311 | headers = {
312 | "user-agent": user_agent,
313 | "Host": "support.euserv.com",
314 | "origin": "https://support.euserv.com",
315 | "Referer": "https://support.euserv.com/index.iphp",
316 | }
317 |
318 | r = session.post(url, headers=headers, data={
319 | "Submit": "Extend contract",
320 | "sess_id": sess_id,
321 | "ord_no": order_id,
322 | "subaction": "choose_order",
323 | "show_contract_extension": "1",
324 | "choose_order_subaction": "show_contract_details",
325 | })
326 |
327 | r = session.post(url, headers=headers, data={
328 | "sess_id": sess_id,
329 | "subaction": "kc2_customer_contract_details_get_change_plan_dialog",
330 | "ord_id": order_id,
331 | "show_manual_extension_if_available": "1",
332 | })
333 |
334 | # send pin code
335 | request_time = time.time()
336 | log(f'[EUserv] Send pin code to {userId} Time: {unixTimeToDate(request_time)}')
337 | r = session.post(url, headers=headers, data={
338 | "sess_id": sess_id,
339 | "subaction": "show_kc2_security_password_dialog",
340 | "prefix": "kc2_customer_contract_details_extend_contract_",
341 | "type": "1",
342 | })
343 | if 'A PIN has been sent to your email address' in r.text:
344 | log('[EUserv] A PIN has been sent to your email address')
345 | else:
346 | log('[EUserv] Send Email failed !')
347 | return False
348 |
349 | pin_code = wait_for_email(request_time)
350 | if not pin_code: return False
351 |
352 | r = session.post(url, headers=headers, data={
353 | "auth": pin_code,
354 | "sess_id": sess_id,
355 | "subaction": "kc2_security_password_get_token",
356 | "prefix": "kc2_customer_contract_details_extend_contract_",
357 | "type": "1",
358 | "ident": "kc2_customer_contract_details_extend_contract_" + order_id,
359 | })
360 | if not r.json().get("rs") == "success":
361 | return False
362 | token = r.json().get('token').get('value')
363 |
364 | r = session.post(url, headers=headers, data={
365 | "sess_id": sess_id,
366 | "subaction": "kc2_customer_contract_details_get_extend_contract_confirmation_dialog",
367 | "token": token,
368 | })
369 | r = session.post(url, headers=headers, data={
370 | "sess_id": sess_id,
371 | "ord_id": order_id,
372 | "subaction": "kc2_customer_contract_details_extend_contract_term",
373 | "token": token,
374 | })
375 |
376 | time.sleep(5)
377 | return True
378 |
379 |
380 | def check(sess_id: str, session: requests.session):
381 | print("Checking.......")
382 | d = get_servers(sess_id, session)
383 | flag = True
384 | for key, val in d.items():
385 | if val:
386 | flag = False
387 | log("[EUserv] ServerID: %s Renew Failed!" % key)
388 |
389 | if flag:
390 | log("[EUserv] ALL Work Done! Enjoy~")
391 |
392 |
393 | def telegram():
394 | data = (
395 | ('chat_id', TG_USER_ID),
396 | ('text', 'EUserv续期日志\n\n' + desp)
397 | )
398 | response = requests.post('https://' + TG_API_HOST + '/bot' + TG_BOT_TOKEN + '/sendMessage', data=data)
399 | if response.status_code != 200:
400 | print('Telegram Bot 推送失败')
401 | else:
402 | print('Telegram Bot 推送成功')
403 |
404 | if __name__ == "__main__":
405 | if not USERNAME or not PASSWORD:
406 | log("[EUserv] 你没有新增任何账户")
407 | exit(1)
408 | user_list = USERNAME.strip().split()
409 | passwd_list = PASSWORD.strip().split()
410 | if len(user_list) != len(passwd_list):
411 | log("[EUserv] The number of usernames and passwords do not match!")
412 | exit(1)
413 | for i in range(len(user_list)):
414 | userId = user_list[i]
415 | log("*" * 30)
416 | log("[EUserv] 正在续期第 %d 个账号 %s" % (i + 1, userId))
417 | sessid, s = login(user_list[i], passwd_list[i])
418 | if sessid == "-1":
419 | log("[EUserv] 第 %d 个账号登陆失败,请检查登录信息" % (i + 1))
420 | continue
421 | elif not sessid:
422 | continue
423 | SERVERS = get_servers(sessid, s)
424 | log("[EUserv] 检测到第 {} 个账号有 {} 台 VPS,正在尝试续期".format(i + 1, len(SERVERS)))
425 | for k, v in SERVERS.items():
426 | if v:
427 | if not renew(sessid, s, passwd_list[i], k):
428 | log("[EUserv] ServerID: %s 德鸡中弹倒地!" % k)
429 | else:
430 | log("[EUserv] ServerID: %s 德鸡续期成功!" % k)
431 | else:
432 | log("[EUserv] ServerID: %s 无需续期" % k)
433 | time.sleep(15)
434 | check(sessid, s)
435 | time.sleep(5)
436 |
437 | TG_BOT_TOKEN and TG_USER_ID and TG_API_HOST and telegram()
438 |
--------------------------------------------------------------------------------
/gmail_api.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import time
4 | import re
5 | import json
6 |
7 | from googleapiclient.discovery import build
8 | from google_auth_oauthlib.flow import InstalledAppFlow
9 | from google.auth.transport.requests import Request
10 | from google.oauth2.credentials import Credentials
11 |
12 | import os, requests, socks, socket
13 | socks.set_default_proxy()
14 | socket.socket = socks.socksocket
15 |
16 |
17 | dir_name = os.path.dirname(os.path.abspath(__file__)) + os.sep
18 | os.chdir(dir_name)
19 |
20 | SCOPES = ['https://mail.google.com/']
21 |
22 | def gmail_authenticate(userId):
23 | creds = None
24 | # the file token.json stores the user's access and refresh tokens, and is
25 | # created automatically when the authorization flow completes for the first time
26 | token_file = f'token_{userId}.json'
27 | if os.path.exists(token_file):
28 | creds = Credentials.from_authorized_user_file(token_file, SCOPES)
29 | # if there are no (valid) credentials availablle, let the user log in.
30 | if not creds or not creds.valid:
31 | if creds and creds.expired and creds.refresh_token:
32 | creds.refresh(Request())
33 | else:
34 | flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
35 | creds = flow.run_local_server(port=36666)
36 | # save the credentials for the next run
37 | with open(token_file, "w") as token:
38 | token.write(creds.to_json())
39 | return build('gmail', 'v1', credentials=creds)
40 |
41 |
42 | def search_messages(service, query):
43 | result = service.users().messages().list(userId='me',q=query).execute()
44 | messages = []
45 | if 'messages' in result:
46 | messages.extend(result['messages'])
47 | while 'nextPageToken' in result:
48 | page_token = result['nextPageToken']
49 | result = service.users().messages().list(userId='me',q=query, pageToken=page_token).execute()
50 | if 'messages' in result:
51 | messages.extend(result['messages'])
52 | return messages
53 |
54 |
55 | if __name__ == "__main__":
56 | email = sys.argv[1:]
57 | for userId in email:
58 | service = gmail_authenticate(userId)
59 |
60 |
--------------------------------------------------------------------------------