├── uid.txt ├── proxy.txt ├── requirements.txt ├── README.md └── main.py /uid.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /proxy.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | loguru 3 | pyfiglet 4 | websockets==12.0 5 | websockets_proxy 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xGrassBot 2 | This bot connects via multiple HTTP proxies to farm Grass Airdrop Season 2 using a single account. It automatically removes faulty proxies (on/off) and uses the Grass Desktop Node to double the farming points (x2.0). 3 | 4 | ## Installation 5 | 6 | 1. Install ENV 7 | ```bash 8 | sudo apt update -y && apt install -y python3 python3-venv pip 9 | ``` 10 | 11 | 2. Setup resources: 12 | ```bash 13 | git clone https://github.com/officialputuid/xGrassBot && cd xGrassBot 14 | python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt 15 | python3 main.py 16 | ``` 17 | 18 | ## User ID 19 | - If you haven't registered, feel free to use my referral link: ([REGISTER GRASS](https://app.getgrass.io/register/?referralCode=rjztRGaBttAB6Cx)). 20 | - How to Get Your User ID? 21 | - Login and open https://app.getgrass.io/dashboard 22 | - Open Developer Tools in your browser (F12) / Inspect Element. 23 | - In the "Console" tab, type: 24 | `localStorage.getItem('userId');` 25 | - Copy the result without "" or '' and paste it into `uid.txt`. 26 | - For multiple accounts, add each uid on a new line, for example: 27 | ``` 28 | uid1 29 | uid2 30 | uid2 31 | ``` 32 | 33 | ## Proxy 34 | - Fill in `proxy.txt` with the format `protocol://user:pass@host:port`. 35 | - Adjust the number of proxies to use on the following line `36 "ONETIME_PROXY = 100"` 36 | 37 | ## Need Proxy? 38 | 1. Sign up at [Proxies.fo](https://app.proxies.fo/ref/849ec384-ecb5-1151-b4a7-c99276bff848). 39 | 2. Go to [Plans](https://app.proxies.fo/plans) and only purchase the "ISP plan" (Residential plans don’t work). 40 | 3. Top up your balance, or you can directly buy a plan and pay with Crypto! 41 | 4. Go to the Dashboard, select your ISP plan, and click "Generate Proxy." 42 | 5. Set the proxy format to `protocol://username:password@hostname:port` 43 | 6. Choose any number for the proxy count, and paste the proxies into `proxy.txt`. 44 | 45 | ## Donations 46 | - **PayPal**: [Paypal.me/IPJAP](https://www.paypal.com/paypalme/IPJAP) 47 | - **Trakteer**: [Trakteer.id/officialputuid](https://trakteer.id/officialputuid) (ID) 48 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 officialputuid 2 | 3 | import aiohttp 4 | import asyncio 5 | import base64 6 | import datetime 7 | import json 8 | import random 9 | import re 10 | import ssl 11 | import time 12 | import uuid 13 | import websockets 14 | 15 | from loguru import logger 16 | import pyfiglet 17 | from websockets_proxy import Proxy, proxy_connect 18 | 19 | logger.remove() 20 | logger.add( 21 | sink=lambda msg: print(msg, end=''), 22 | format=( 23 | "{time:DD/MM/YY HH:mm:ss} | " 24 | "{level:8} | {message}" 25 | ), 26 | colorize=True 27 | ) 28 | 29 | # main.py 30 | def print_header(): 31 | cn = pyfiglet.figlet_format("xGrassBot") 32 | print(cn) 33 | print("🌱 Season 2") 34 | print("🎨 by \033]8;;https://github.com/officialputuid\033\\officialputuid\033]8;;\033\\") 35 | print('🎁 \033]8;;https://paypal.me/IPJAP\033\\Paypal.me/IPJAP\033]8;;\033\\ — \033]8;;https://trakteer.id/officialputuid\033\\Trakteer.id/officialputuid\033]8;;\033\\') 36 | 37 | # Initialize the header 38 | print_header() 39 | 40 | # Number of proxies to use /uid 41 | ONETIME_PROXY = 100 42 | DELAY_INTERVAL = 0.5 43 | MAX_RETRIES = 3 44 | FILE_UID = "uid.txt" 45 | FILE_PROXY = "proxy.txt" 46 | USERAGENTS = [ 47 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.2365.57", 48 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.2365.52", 49 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.2365.46", 50 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.2277.128", 51 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.2277.112", 52 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.2277.98", 53 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.2277.83", 54 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.133", 55 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.121", 56 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.91" 57 | ] 58 | HTTP_STATUS_CODES = { 59 | 200: "OK", 60 | 201: "Created", 61 | 202: "Accepted", 62 | 204: "No Content", 63 | 400: "Bad Request", 64 | 401: "Unauthorized", 65 | 403: "Forbidden", 66 | 404: "Not Found", 67 | 500: "Internal Server Error", 68 | 502: "Bad Gateway", 69 | 503: "Service Unavailable", 70 | 504: "Gateway Timeout" 71 | } 72 | 73 | # Read UID and Proxy count 74 | def read_uid_and_proxy(): 75 | with open(FILE_UID, 'r') as file: 76 | uid_count = sum(1 for line in file) 77 | 78 | with open(FILE_PROXY, 'r') as file: 79 | proxy_count = sum(1 for line in file) 80 | 81 | return uid_count, proxy_count 82 | 83 | uid_count, proxy_count = read_uid_and_proxy() 84 | 85 | print() 86 | print(f"🔑 UID: {uid_count}. from {FILE_UID}.") 87 | print(f"🌐 Loaded {proxy_count} proxies. from {FILE_PROXY}.") 88 | print(f"🌐 Active proxy loaded per-task: {ONETIME_PROXY} proxies.") 89 | print() 90 | 91 | # Get User input for proxy failure handling 92 | def get_user_input(): 93 | user_input = "" 94 | while user_input not in ['yes', 'no']: 95 | user_input = input("🔵 Do you want to remove the proxy if there is a specific failure (yes/no)? ").strip().lower() 96 | if user_input not in ['yes', 'no']: 97 | print("🔴 Invalid input. Please enter 'yes' or 'no'.") 98 | return user_input == 'yes' 99 | 100 | remove_on_all_errors = get_user_input() 101 | print(f"🔵 You selected: {'Yes' if remove_on_all_errors else 'No'}, ENJOY!\n") 102 | 103 | # Ask user for node type 104 | def get_node_type(): 105 | node_type = "" 106 | while node_type not in ['desktop', 'extension', 'grasslite']: 107 | print(f"🧩 Desktop Node (2.0x Reward), Extension (1.25x Reward), GrassLite (1.0x Reward)") 108 | node_type = input("🔵 Choose node type (desktop/extension/grasslite): ").strip().lower() 109 | if node_type not in ['desktop', 'extension', 'grasslite']: 110 | print("🔴 Invalid input. Please enter 'desktop' / 'extension' / 'grasslite'.") 111 | return node_type 112 | 113 | node_type = get_node_type() 114 | print(f"🔵 You selected: {node_type.capitalize()} node. ENJOY!\n") 115 | 116 | def truncate_userid(user_id): 117 | return f"{user_id[:3]}--{user_id[-3:]}" 118 | 119 | def truncate_proxy(proxy): 120 | pattern = r'([a-zA-Z0-9.-]+(?:\.[a-zA-Z]{2,})|(?:\d{1,3}\.){3}\d{1,3})' 121 | match = re.search(pattern, proxy) 122 | if match: 123 | return match.group(0) 124 | return 'Undefined' 125 | 126 | def count_proxies(FILE_PROXY): 127 | try: 128 | with open(FILE_PROXY, 'r') as file: 129 | proxies = file.readlines() 130 | return len(proxies) 131 | except FileNotFoundError: 132 | logger.error(f"File {FILE_PROXY} not found!") 133 | return 0 134 | 135 | async def connect_to_wss(protocol_proxy, user_id): 136 | device_id = str(uuid.uuid3(uuid.NAMESPACE_DNS, protocol_proxy)) 137 | random_user_agent = random.choice(USERAGENTS) 138 | logger.info(f"UID: {truncate_userid(user_id)} | {node_type} | Generate Device ID: {device_id} | Proxy: {truncate_proxy(protocol_proxy)}") 139 | 140 | has_received_action = False 141 | is_authenticated = False 142 | 143 | total_proxies = count_proxies(FILE_PROXY) 144 | 145 | while True: 146 | try: 147 | await asyncio.sleep(random.randint(1, 10) / 10) 148 | custom_headers = { 149 | "User-Agent": random_user_agent 150 | } 151 | 152 | if node_type == "extension": 153 | custom_headers["Origin"] = "chrome-extension://lkbnfiajjmbhnfledhphioinpickokdi" 154 | elif node_type == "grasslite": 155 | custom_headers["Origin"] = "chrome-extension://ilehaonighjijnmpnagapkhpcdbhclfg" 156 | 157 | ssl_context = ssl.create_default_context() 158 | ssl_context.check_hostname = False 159 | ssl_context.verify_mode = ssl.CERT_NONE 160 | urilist = [ 161 | "wss://proxy2.wynd.network:4444", 162 | "wss://proxy2.wynd.network:4650" 163 | ] 164 | uri = random.choice(urilist) 165 | server_hostname = uri.split("://")[1].split(":")[0] 166 | proxy = Proxy.from_url(protocol_proxy) 167 | 168 | async with proxy_connect( 169 | uri, 170 | proxy=proxy, 171 | ssl=ssl_context, 172 | server_hostname=server_hostname, 173 | extra_headers=custom_headers 174 | ) as websocket: 175 | logger.success(f"UID: {truncate_userid(user_id)} | {node_type} | Success connect to WS | uri: {uri} | Headers: {custom_headers} | Device ID: {device_id} | Proxy: {truncate_proxy(protocol_proxy)} | Remaining Proxy: {total_proxies}") 176 | 177 | async def send_ping(): 178 | while True: 179 | if has_received_action: 180 | send_message = json.dumps({ 181 | "id": str(uuid.uuid4()), 182 | "version": "1.0.0", 183 | "action": "PING", 184 | "data": {} 185 | }) 186 | logger.debug(f"UID: {truncate_userid(user_id)} | {node_type} | Send PING message | data: {send_message}") 187 | await asyncio.sleep(DELAY_INTERVAL) 188 | await websocket.send(send_message) 189 | logger.info(f"UID: {truncate_userid(user_id)} | {node_type} | Done sent PING | data: {send_message}") 190 | 191 | rand_sleep = random.uniform(10, 30) 192 | logger.info(f"UID: {truncate_userid(user_id)} | {node_type} | Next PING in {rand_sleep:.2f} seconds, ENJOY!") 193 | await asyncio.sleep(rand_sleep) 194 | 195 | await asyncio.sleep(DELAY_INTERVAL) 196 | send_ping_task = asyncio.create_task(send_ping()) 197 | 198 | try: 199 | while True: 200 | if is_authenticated and not has_received_action: 201 | logger.info(f"UID: {truncate_userid(user_id)} | {node_type} | Authenticated | Wait for PING Gate to Open for {'HTTP_REQUEST' if node_type in ['extension', 'grasslite'] else 'OPEN_TUNNEL'}") 202 | 203 | response = await websocket.recv() 204 | message = json.loads(response) 205 | logger.info(f"UID: {truncate_userid(user_id)} | {node_type} | Received message | data: {message}") 206 | 207 | if message.get("action") == "AUTH": 208 | extension_ids = { 209 | "extension": "lkbnfiajjmbhnfledhphioinpickokdi", 210 | "grasslite": "ilehaonighjijnmpnagapkhpcdbhclfg" 211 | } 212 | 213 | auth_response = { 214 | "id": message["id"], 215 | "origin_action": "AUTH", 216 | "result": { 217 | "browser_id": device_id, 218 | "user_id": user_id, 219 | "user_agent": random_user_agent, 220 | "timestamp": int(time.time()), 221 | "device_type": "extension" if node_type in ["extension", "grasslite"] else "desktop", 222 | "version": "4.26.2" if node_type == "extension" else "4.30.0" 223 | } 224 | } 225 | 226 | if node_type in extension_ids: 227 | auth_response["result"]["extension_id"] = extension_ids[node_type] 228 | 229 | logger.debug(f"UID: {truncate_userid(user_id)} | {node_type} | Send AUTH | data: {auth_response}") 230 | await asyncio.sleep(DELAY_INTERVAL) 231 | await websocket.send(json.dumps(auth_response)) 232 | logger.success(f"UID: {truncate_userid(user_id)} | {node_type} | Done sent AUTH | data: {auth_response}") 233 | is_authenticated = True 234 | 235 | elif message.get("action") in ["HTTP_REQUEST", "OPEN_TUNNEL"]: 236 | has_received_action = True 237 | request_data = message["data"] 238 | 239 | headers = { 240 | "User-Agent": custom_headers["User-Agent"], 241 | "Content-Type": "application/json; charset=utf-8" 242 | } 243 | 244 | async with aiohttp.ClientSession() as session: 245 | async with session.get(request_data["url"], headers=headers) as api_response: 246 | content = await api_response.text() 247 | encoded_body = base64.b64encode(content.encode()).decode() 248 | 249 | status_text = HTTP_STATUS_CODES.get(api_response.status, "") 250 | 251 | http_response = { 252 | "id": message["id"], 253 | "origin_action": message["action"], 254 | "result": { 255 | "url": request_data["url"], 256 | "status": api_response.status, 257 | "status_text": status_text, 258 | "headers": dict(api_response.headers), 259 | "body": encoded_body 260 | } 261 | } 262 | 263 | logger.info(f"UID: {truncate_userid(user_id)} | {node_type} | Open PING Access | data: {http_response}") 264 | await asyncio.sleep(DELAY_INTERVAL) 265 | await websocket.send(json.dumps(http_response)) 266 | logger.success(f"UID: {truncate_userid(user_id)} | {node_type} | Done sent PING Access | data: {http_response}") 267 | 268 | elif message.get("action") == "PONG": 269 | pong_response = {"id": message["id"], "origin_action": "PONG"} 270 | logger.debug(f"UID: {truncate_userid(user_id)} | {node_type} | Send PONG | data: {pong_response}") 271 | await asyncio.sleep(DELAY_INTERVAL) 272 | await websocket.send(json.dumps(pong_response)) 273 | logger.success(f"UID: {truncate_userid(user_id)} | {node_type} | Done sent PONG | data: {pong_response}") 274 | 275 | except websockets.exceptions.ConnectionClosedError as e: 276 | logger.error(f"UID: {truncate_userid(user_id)} | {node_type} | Connection closed error | Proxy: {truncate_proxy(protocol_proxy)} | Error: {str(e)} | Remaining Proxy: {total_proxies}") 277 | await asyncio.sleep(DELAY_INTERVAL) 278 | finally: 279 | await websocket.close() 280 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | WebSocket connection closed | Proxy: {truncate_proxy(protocol_proxy)} | Remaining Proxy: {total_proxies}") 281 | send_ping_task.cancel() 282 | await asyncio.sleep(DELAY_INTERVAL) 283 | break 284 | 285 | except Exception as e: 286 | logger.error(f"UID: {truncate_userid(user_id)} | {node_type} | Error with proxy {truncate_proxy(protocol_proxy)} ➜ {str(e)} | Remaining Proxy: {total_proxies}") 287 | error_conditions = [ 288 | "403 Forbidden", 289 | "Host unreachable", 290 | "Empty host component", 291 | "Invalid scheme component", 292 | "[SSL: WRONG_VERSION_NUMBER]", 293 | "invalid length of packed IP address string", 294 | "Empty connect reply", 295 | "Device creation limit exceeded", 296 | "[Errno 111] Could not connect to proxy", 297 | "sent 1011 (internal error) keepalive ping timeout; no close frame received" 298 | ] 299 | skip_proxy = [ 300 | "Proxy connection timed out: 60", 301 | "407 Proxy Authentication Required", 302 | "Invalid port component" 303 | ] 304 | 305 | if any(error_msg in str(e) for error_msg in skip_proxy): 306 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | Skipping proxy due to error ➜ {truncate_proxy(protocol_proxy)} | Remaining Proxy: {total_proxies}") 307 | return "skip" 308 | 309 | if remove_on_all_errors: 310 | if any(error_msg in str(e) for error_msg in error_conditions): 311 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | Removing error proxy due to error ➜ {truncate_proxy(protocol_proxy)} | Remaining Proxy: {total_proxies}") 312 | remove_proxy_from_list(protocol_proxy) 313 | return None 314 | else: 315 | if "Device creation limit exceeded" in str(e): 316 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | Removing error proxy due to error ➜ {truncate_proxy(protocol_proxy)} | Remaining Proxy: {total_proxies}") 317 | remove_proxy_from_list(protocol_proxy) 318 | return None 319 | 320 | await asyncio.sleep(DELAY_INTERVAL) 321 | continue 322 | 323 | async def main(): 324 | with open(FILE_UID, 'r') as file: 325 | user_ids = file.read().splitlines() 326 | 327 | with open(FILE_PROXY, 'r') as file: 328 | all_proxies = file.read().splitlines() 329 | 330 | if len(all_proxies) < ONETIME_PROXY * len(user_ids): 331 | logger.error(f"The number of proxies is insufficient to provide {ONETIME_PROXY} proxies per User ID.") 332 | return 333 | 334 | random.shuffle(all_proxies) 335 | proxy_allocation = { 336 | user_id: all_proxies[i * ONETIME_PROXY: (i + 1) * ONETIME_PROXY] 337 | for i, user_id in enumerate(user_ids) 338 | } 339 | 340 | retry_count = {} 341 | 342 | for user_id, proxies in proxy_allocation.items(): 343 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | Total proxies to be used: {len(proxies)}") 344 | await asyncio.sleep(DELAY_INTERVAL) 345 | 346 | tasks = {} 347 | 348 | for user_id, proxies in proxy_allocation.items(): 349 | for proxy in proxies: 350 | retry_count[(proxy, user_id)] = 0 351 | await asyncio.sleep(DELAY_INTERVAL) 352 | task = asyncio.create_task(connect_to_wss(proxy, user_id)) 353 | tasks[task] = (proxy, user_id) 354 | 355 | while True: 356 | done, pending = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED) 357 | 358 | for task in done: 359 | try: 360 | result = task.result() 361 | 362 | failed_proxy, user_id = tasks[task] 363 | 364 | if result == "skip": 365 | retry_count[(failed_proxy, user_id)] += 1 366 | 367 | if retry_count[(failed_proxy, user_id)] > MAX_RETRIES: 368 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | Max retries (skipping proxy) reached for proxy: {truncate_proxy(failed_proxy)}.") 369 | continue 370 | 371 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | Skipping proxy: {truncate_proxy(failed_proxy)}") 372 | 373 | available_proxies = list(set(all_proxies) - set(proxy_allocation[user_id])) 374 | if available_proxies: 375 | new_proxy = random.choice(available_proxies) 376 | proxy_allocation[user_id].append(new_proxy) 377 | 378 | retry_count[(new_proxy, user_id)] = retry_count[(failed_proxy, user_id)] 379 | await asyncio.sleep(DELAY_INTERVAL) 380 | new_task = asyncio.create_task(connect_to_wss(new_proxy, user_id)) 381 | tasks[new_task] = (new_proxy, user_id) 382 | logger.success(f"UID: {truncate_userid(user_id)} | {node_type} | Replaced skipping proxy {truncate_proxy(failed_proxy)} with {truncate_proxy(new_proxy)}") 383 | else: 384 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | No available proxies left for replacement.") 385 | 386 | elif result is None: 387 | retry_count[(failed_proxy, user_id)] += 1 388 | 389 | if retry_count[(failed_proxy, user_id)] > MAX_RETRIES: 390 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | Max retries (error proxy) reached for proxy: {truncate_proxy(failed_proxy)}.") 391 | proxy_allocation[user_id].remove(failed_proxy) 392 | continue 393 | 394 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | Removing and replacing failed proxy: {truncate_proxy(failed_proxy)}") 395 | proxy_allocation[user_id].remove(failed_proxy) 396 | 397 | available_proxies = list(set(all_proxies) - set(proxy_allocation[user_id])) 398 | if available_proxies: 399 | new_proxy = random.choice(available_proxies) 400 | proxy_allocation[user_id].append(new_proxy) 401 | 402 | retry_count[(new_proxy, user_id)] = retry_count[(failed_proxy, user_id)] 403 | await asyncio.sleep(DELAY_INTERVAL) 404 | new_task = asyncio.create_task(connect_to_wss(new_proxy, user_id)) 405 | tasks[new_task] = (new_proxy, user_id) 406 | logger.success(f"UID: {truncate_userid(user_id)} | {node_type} | Replaced failed proxy: {truncate_proxy(failed_proxy)} with: {truncate_proxy(new_proxy)}") 407 | else: 408 | logger.warning(f"UID: {truncate_userid(user_id)} | {node_type} | No available proxies left for replacement.") 409 | 410 | except Exception as e: 411 | logger.error(f"UID: {truncate_userid(user_id)} | {node_type} | Error handling task: {str(e)}") 412 | finally: 413 | tasks.pop(task) 414 | 415 | active_proxies = [proxy for _, proxy in tasks.values()] 416 | for user_id, proxies in proxy_allocation.items(): 417 | for proxy in set(proxies) - set(active_proxies): 418 | new_task = asyncio.create_task(connect_to_wss(proxy, user_id)) 419 | tasks[new_task] = (proxy, user_id) 420 | 421 | def remove_proxy_from_list(proxy): 422 | with open(FILE_PROXY, "r+") as file: 423 | lines = file.readlines() 424 | file.seek(0) 425 | for line in lines: 426 | if line.strip() != proxy: 427 | file.write(line) 428 | file.truncate() 429 | 430 | if __name__ == '__main__': 431 | try: 432 | asyncio.run(main()) 433 | except (KeyboardInterrupt, SystemExit): 434 | logger.info(f"Program terminated by user. ENJOY!\n") 435 | --------------------------------------------------------------------------------