├── 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 | ![py.png](https://s2.loli.net/2022/11/12/PmqVFTIkibHUzhZ.png)
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 | ![tc.png](https://s2.loli.net/2022/11/13/7vZgEBMUAzbSJea.png) 80 | ![cc.png](https://s2.loli.net/2022/11/13/aEdLKjcS5C7bpZf.png) 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 | --------------------------------------------------------------------------------