Merge pull request #163 from izzzzzi/main
Рефакторинг и оптимизация кода
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Сохраняем текущие изменения
|
||||
echo "Сохранение текущих изменений..."
|
||||
git stash -q --keep-index
|
||||
|
||||
# Запускаем форматирование кода
|
||||
echo "Запуск Ruff format..."
|
||||
ruff format . --config pyproject.toml --exclude main.py,handlers/payments
|
||||
|
||||
# Запускаем проверку и исправление кода
|
||||
echo "Запуск Ruff check с автоисправлением..."
|
||||
ruff check . --config pyproject.toml --exclude main.py,handlers/payments --fix
|
||||
|
||||
# Добавляем изменения, внесенные форматированием
|
||||
git add -u
|
||||
|
||||
# Восстанавливаем сохраненные изменения
|
||||
echo "Восстановление сохраненных изменений..."
|
||||
git stash pop -q
|
||||
|
||||
# Выход с кодом 0 (успешно)
|
||||
exit 0
|
||||
@@ -1,14 +1,16 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import aiofiles
|
||||
|
||||
from aiogram.types import BufferedInputFile
|
||||
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT
|
||||
|
||||
from bot import bot
|
||||
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT
|
||||
from logger import logger
|
||||
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ from aiogram.filters import ExceptionTypeFilter
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.types import BufferedInputFile, ErrorEvent
|
||||
from aiogram.utils.markdown import hbold
|
||||
|
||||
from config import ADMIN_ID, API_TOKEN
|
||||
|
||||
from filters.private import IsPrivateFilter
|
||||
from logger import logger
|
||||
from middlewares import register_middleware
|
||||
|
||||
|
||||
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
||||
storage = MemoryStorage()
|
||||
dp = Dispatcher(bot=bot, storage=storage)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
import httpx
|
||||
|
||||
import httpx
|
||||
import py3xui
|
||||
|
||||
from config import LIMIT_IP, SUPERNODE
|
||||
|
||||
from logger import logger
|
||||
|
||||
|
||||
@@ -54,7 +56,7 @@ async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, An
|
||||
logger.info(f"Клиент {config.email} успешно добавлен с ID {config.client_id}")
|
||||
|
||||
return response if response else {"status": "failed"}
|
||||
|
||||
|
||||
except httpx.ConnectTimeout as e:
|
||||
logger.error(f"Ошибка при добавлении клиента {config.email}: {e}")
|
||||
return {"status": "failed", "error": "Timeout"}
|
||||
@@ -114,7 +116,7 @@ async def extend_client_key(
|
||||
await xui.client.reset_stats(inbound_id, email)
|
||||
logger.info(f"Ключ клиента {email} успешно продлён до {new_expiry_time}")
|
||||
return True
|
||||
|
||||
|
||||
except httpx.ConnectTimeout as e:
|
||||
logger.error(f"Ошибка при обновлении клиента {email}: {e}")
|
||||
return False
|
||||
@@ -159,7 +161,7 @@ async def delete_client(
|
||||
await xui.client.delete(inbound_id, client.id)
|
||||
logger.info(f"Клиент с ID {client_id} был удален успешно")
|
||||
return True
|
||||
|
||||
|
||||
except httpx.ConnectTimeout as e:
|
||||
logger.error(f"Ошибка при удалении клиента {email}: {e}")
|
||||
return False
|
||||
@@ -190,7 +192,7 @@ async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str,
|
||||
|
||||
logger.info(f"Трафик для клиента {client_id} успешно получен.")
|
||||
return {"status": "success", "client_id": client_id, "traffic": traffic_data}
|
||||
|
||||
|
||||
except httpx.ConnectTimeout as e:
|
||||
logger.error(f"Ошибка при получении трафика клиента {client_id}: {e}")
|
||||
return {"status": "error", "error": "Timeout"}
|
||||
@@ -241,7 +243,7 @@ async def toggle_client(xui: py3xui.AsyncApi, inbound_id: int, email: str, clien
|
||||
status = "включении" if enable else "отключении"
|
||||
logger.error(f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
except Exception as e:
|
||||
status = "включении" if enable else "отключении"
|
||||
logger.error(f"Ошибка при {status} клиента с email {email} и ID {client_id}: {e}")
|
||||
|
||||
+21
-25
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
@@ -6,6 +7,7 @@ import asyncpg
|
||||
import pytz
|
||||
|
||||
from config import CASHBACK, CHECK_REFERRAL_REWARD_ISSUED, DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
|
||||
|
||||
from logger import logger
|
||||
|
||||
|
||||
@@ -674,7 +676,7 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
|
||||
"""
|
||||
|
||||
if amount <= 0:
|
||||
return
|
||||
return
|
||||
conn = None
|
||||
try:
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
@@ -852,14 +854,12 @@ async def get_total_referral_bonus(conn, referrer_tg_id: int, max_levels: int) -
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
{
|
||||
" ".join(
|
||||
[
|
||||
f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]} * ep.amount"
|
||||
if isinstance(REFERRAL_BONUS_PERCENTAGES[level], float)
|
||||
else f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}"
|
||||
for level in REFERRAL_BONUS_PERCENTAGES
|
||||
]
|
||||
)
|
||||
" ".join([
|
||||
f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]} * ep.amount"
|
||||
if isinstance(REFERRAL_BONUS_PERCENTAGES[level], float)
|
||||
else f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}"
|
||||
for level in REFERRAL_BONUS_PERCENTAGES
|
||||
])
|
||||
}
|
||||
ELSE 0
|
||||
END
|
||||
@@ -898,14 +898,12 @@ async def get_total_referral_bonus(conn, referrer_tg_id: int, max_levels: int) -
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
{
|
||||
" ".join(
|
||||
[
|
||||
f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]} * p.amount"
|
||||
if isinstance(REFERRAL_BONUS_PERCENTAGES[level], float)
|
||||
else f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}"
|
||||
for level in REFERRAL_BONUS_PERCENTAGES
|
||||
]
|
||||
)
|
||||
" ".join([
|
||||
f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]} * p.amount"
|
||||
if isinstance(REFERRAL_BONUS_PERCENTAGES[level], float)
|
||||
else f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}"
|
||||
for level in REFERRAL_BONUS_PERCENTAGES
|
||||
])
|
||||
}
|
||||
ELSE 0
|
||||
END
|
||||
@@ -1339,14 +1337,12 @@ async def get_servers(session: Any = None):
|
||||
if cluster_name not in servers:
|
||||
servers[cluster_name] = []
|
||||
|
||||
servers[cluster_name].append(
|
||||
{
|
||||
"server_name": row["server_name"],
|
||||
"api_url": row["api_url"],
|
||||
"subscription_url": row["subscription_url"],
|
||||
"inbound_id": row["inbound_id"],
|
||||
}
|
||||
)
|
||||
servers[cluster_name].append({
|
||||
"server_name": row["server_name"],
|
||||
"api_url": row["api_url"],
|
||||
"subscription_url": row["subscription_url"],
|
||||
"inbound_id": row["inbound_id"],
|
||||
})
|
||||
|
||||
return servers
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
from config import ADMIN_ID
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from .payments import router as payments_router
|
||||
from .profile import router as profile_router
|
||||
from .start import router as start_router
|
||||
|
||||
|
||||
router = Router(name="handlers_main_router")
|
||||
|
||||
router.include_routers(
|
||||
|
||||
@@ -12,6 +12,7 @@ from .admin_servers import router as servers_router
|
||||
from .admin_stats import router as stats_router
|
||||
from .admin_users import router as users_router
|
||||
|
||||
|
||||
router = Router(name="admins_main_router")
|
||||
|
||||
router.include_routers(
|
||||
|
||||
@@ -5,6 +5,7 @@ from backup import backup_database
|
||||
from filters.admin import IsAdminFilter
|
||||
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from filters.admin import IsAdminFilter
|
||||
from keyboards.admin.bans_kb import build_bans_kb
|
||||
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
@@ -4,14 +4,15 @@ from aiogram import F, Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from config import USERNAME_BOT
|
||||
|
||||
from database import create_coupon, delete_coupon, get_all_coupons
|
||||
from filters.admin import IsAdminFilter
|
||||
from keyboards.admin.coupons_kb import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb
|
||||
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
|
||||
from logger import logger
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from bot import version
|
||||
from filters.admin import IsAdminFilter
|
||||
from keyboards.admin.panel_kb import AdminPanelCallback, build_management_kb, build_panel_kb
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from aiogram.types import CallbackQuery
|
||||
from filters.admin import IsAdminFilter
|
||||
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb, build_restart_kb
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
|
||||
from keyboards.admin.sender_kb import AdminSenderCallback, build_clusters_kb, build_sender_kb
|
||||
from logger import logger
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import asyncio
|
||||
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
|
||||
from py3xui import AsyncApi
|
||||
|
||||
from backup import create_backup_and_send_to_admins
|
||||
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
|
||||
from database import check_unique_server_name, delete_server, get_servers
|
||||
from filters.admin import IsAdminFilter
|
||||
from handlers.keys.key_utils import create_key_on_cluster
|
||||
@@ -23,6 +25,7 @@ from keyboards.admin.servers_kb import (
|
||||
)
|
||||
from logger import logger
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import pytz
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.types import CallbackQuery
|
||||
@@ -12,6 +13,7 @@ from keyboards.admin.stats_kb import build_stats_kb
|
||||
from logger import logger
|
||||
from utils.csv_export import export_payments_csv, export_users_csv
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import asyncio
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
import pytz
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from config import TOTAL_GB
|
||||
|
||||
from database import (
|
||||
delete_key,
|
||||
delete_user_data,
|
||||
@@ -40,6 +42,7 @@ from keyboards.admin.users_kb import (
|
||||
)
|
||||
from logger import logger
|
||||
|
||||
|
||||
MOSCOW_TZ = pytz.timezone("Europe/Moscow")
|
||||
|
||||
router = Router()
|
||||
|
||||
+3
-1
@@ -1,17 +1,19 @@
|
||||
import random
|
||||
import secrets
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import CAPTCHA_EMOJIS
|
||||
|
||||
from logger import logger
|
||||
|
||||
from .utils import edit_or_send_message
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -3,8 +3,8 @@ from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton, LabeledPrice, Message, PreCheckoutQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import RUB_TO_XTR
|
||||
|
||||
from logger import logger
|
||||
|
||||
from .utils import edit_or_send_message
|
||||
|
||||
@@ -4,6 +4,7 @@ from aiogram import Router
|
||||
|
||||
from .instructions import router as instructions_router
|
||||
|
||||
|
||||
router = Router(name="instructions_main_router")
|
||||
|
||||
router.include_routers(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiogram import F, Router, types
|
||||
@@ -8,18 +9,19 @@ from aiogram.types import (
|
||||
Message,
|
||||
)
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import CONNECT_MACOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL
|
||||
|
||||
from database import get_key_details
|
||||
from handlers.texts import (
|
||||
CONNECT_TV_TEXT,
|
||||
INSTRUCTION_PC,
|
||||
INSTRUCTIONS,
|
||||
INSTRUCTION_PC,
|
||||
KEY_MESSAGE,
|
||||
SUBSCRIPTION_DETAILS_TEXT,
|
||||
)
|
||||
from handlers.utils import edit_or_send_message
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@@ -80,7 +82,6 @@ async def process_connect_pc(callback_query: CallbackQuery, session: Any):
|
||||
)
|
||||
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("connect_tv|"))
|
||||
async def process_connect_tv(callback_query: CallbackQuery):
|
||||
key_name = callback_query.data.split("|")[1]
|
||||
@@ -102,7 +103,6 @@ async def process_connect_tv(callback_query: CallbackQuery):
|
||||
@router.callback_query(F.data.startswith("continue_tv|"))
|
||||
async def process_continue_tv(callback_query: CallbackQuery, session: Any):
|
||||
key_name = callback_query.data.split("|")[1]
|
||||
tg_id = callback_query.from_user.id
|
||||
|
||||
record = await get_key_details(key_name, session)
|
||||
subscription_link = record["key"]
|
||||
|
||||
@@ -5,6 +5,7 @@ from aiogram import Router
|
||||
from .key_management import router as management_router
|
||||
from .keys import router as keys_router
|
||||
|
||||
|
||||
router = Router(name="keys_main_router")
|
||||
|
||||
router.include_routers(
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import pytz
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from py3xui import AsyncApi
|
||||
|
||||
from bot import bot
|
||||
from client import delete_client
|
||||
from config import (
|
||||
ADMIN_PASSWORD,
|
||||
ADMIN_USERNAME,
|
||||
@@ -29,6 +27,10 @@ from config import (
|
||||
USE_COUNTRY_SELECTION,
|
||||
USE_NEW_PAYMENT_FLOW,
|
||||
)
|
||||
from py3xui import AsyncApi
|
||||
|
||||
from bot import bot
|
||||
from client import delete_client
|
||||
from database import (
|
||||
add_connection,
|
||||
check_connection_exists,
|
||||
@@ -55,6 +57,7 @@ from handlers.texts import DISCOUNTS, key_message_success
|
||||
from handlers.utils import edit_or_send_message, generate_random_email, get_least_loaded_cluster
|
||||
from logger import logger
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
moscow_tz = pytz.timezone("Europe/Moscow")
|
||||
@@ -91,9 +94,11 @@ async def handle_key_creation(
|
||||
updated = await update_trial(tg_id, 1, session)
|
||||
if updated:
|
||||
await edit_or_send_message(
|
||||
target_message=message_or_query if isinstance(message_or_query, Message) else message_or_query.message,
|
||||
target_message=message_or_query
|
||||
if isinstance(message_or_query, Message)
|
||||
else message_or_query.message,
|
||||
text="⏳ Пожалуйста, подождите, создаем вам подключение...",
|
||||
reply_markup=None,
|
||||
reply_markup=None,
|
||||
)
|
||||
|
||||
await create_key(tg_id, expiry_time, state, session, message_or_query)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import asyncio
|
||||
|
||||
from typing import Any
|
||||
|
||||
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, USE_COUNTRY_SELECTION
|
||||
from py3xui import AsyncApi
|
||||
|
||||
from client import ClientConfig, add_client, delete_client, extend_client_key, get_client_traffic, toggle_client
|
||||
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, USE_COUNTRY_SELECTION
|
||||
from database import get_servers, store_key
|
||||
from handlers.utils import get_least_loaded_cluster
|
||||
from logger import logger
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import asyncio
|
||||
import locale
|
||||
import os
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
import pytz
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot import bot
|
||||
from config import (
|
||||
CONNECT_ANDROID,
|
||||
CONNECT_IOS,
|
||||
@@ -26,6 +26,8 @@ from config import (
|
||||
USE_COUNTRY_SELECTION,
|
||||
USE_NEW_PAYMENT_FLOW,
|
||||
)
|
||||
|
||||
from bot import bot
|
||||
from database import (
|
||||
check_server_name_by_cluster,
|
||||
create_temporary_data,
|
||||
@@ -64,6 +66,7 @@ from handlers.texts import (
|
||||
from handlers.utils import edit_or_send_message, handle_error
|
||||
from logger import logger
|
||||
|
||||
|
||||
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
|
||||
|
||||
router = Router()
|
||||
@@ -382,7 +385,7 @@ async def process_callback_confirm_delete(callback_query: CallbackQuery, session
|
||||
servers = await get_servers(session)
|
||||
|
||||
async def delete_key_from_servers():
|
||||
try: # lol
|
||||
try: # lol
|
||||
tasks = []
|
||||
for cluster_id, _cluster in servers.items():
|
||||
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
|
||||
|
||||
@@ -4,13 +4,14 @@ import random
|
||||
import re
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import aiohttp
|
||||
import asyncpg
|
||||
import pytz
|
||||
from aiohttp import web
|
||||
|
||||
from aiohttp import web
|
||||
from config import (
|
||||
DATABASE_URL,
|
||||
PROJECT_NAME,
|
||||
@@ -19,9 +20,10 @@ from config import (
|
||||
SUPPORT_CHAT_URL,
|
||||
TOTAL_GB,
|
||||
TRANSITION_DATE_STR,
|
||||
USE_COUNTRY_SELECTION,
|
||||
USERNAME_BOT,
|
||||
USE_COUNTRY_SELECTION,
|
||||
)
|
||||
|
||||
from database import get_key_details, get_servers
|
||||
from handlers.utils import convert_to_bytes
|
||||
from logger import logger
|
||||
@@ -275,7 +277,7 @@ def prepare_headers(
|
||||
"subscription-userinfo": subscription_userinfo,
|
||||
}
|
||||
elif "Hiddify" in user_agent:
|
||||
parts = subscription_info.split(' - ')[0].split(': ')
|
||||
parts = subscription_info.split(" - ")[0].split(": ")
|
||||
key_info = parts[1] if len(parts) > 1 else parts[0]
|
||||
|
||||
encoded_project_name = f"{project_name}\n📄 Подписка: {key_info}"
|
||||
|
||||
@@ -5,6 +5,7 @@ from aiogram import Router
|
||||
from .general_notifications import router as general_notifications_router
|
||||
from .special_notifications import router as special_notifications_router
|
||||
|
||||
|
||||
router = Router(name="notifications_main_router")
|
||||
|
||||
router.include_routers(general_notifications_router, special_notifications_router)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import asyncio
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import asyncpg
|
||||
import pytz
|
||||
from aiogram import Bot, Router
|
||||
|
||||
from aiogram import Bot, Router
|
||||
from config import (
|
||||
DATABASE_URL,
|
||||
NOTIFICATION_TIME,
|
||||
@@ -17,6 +18,7 @@ from config import (
|
||||
TOTAL_GB,
|
||||
TRIAL_TIME_DISABLE,
|
||||
)
|
||||
|
||||
from database import (
|
||||
add_notification,
|
||||
check_notification_time,
|
||||
@@ -35,6 +37,7 @@ from logger import logger
|
||||
from .notify_utils import send_notification
|
||||
from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
moscow_tz = pytz.timezone("Europe/Moscow")
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import os
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import aiofiles
|
||||
from aiogram import Bot, types
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.exceptions import TelegramForbiddenError
|
||||
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup
|
||||
|
||||
@@ -13,40 +16,68 @@ async def send_notification(
|
||||
tg_id: int,
|
||||
image_filename: str,
|
||||
caption: str,
|
||||
keyboard: InlineKeyboardMarkup,
|
||||
):
|
||||
keyboard: InlineKeyboardMarkup | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Отправляет уведомление с изображением, если файл существует, иначе отправляет текстовое сообщение.
|
||||
Если возникает TelegramForbiddenError (например, бот заблокирован пользователем),
|
||||
функция логирует ошибку и прекращает попытки отправки уведомления.
|
||||
Отправляет уведомление пользователю.
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота для отправки сообщений
|
||||
tg_id: Telegram ID пользователя
|
||||
image_filename: Имя файла изображения в директории img
|
||||
caption: Текст сообщения
|
||||
keyboard: Клавиатура для сообщения (опционально)
|
||||
|
||||
Returns:
|
||||
bool: True если сообщение успешно отправлено, False в случае ошибки
|
||||
"""
|
||||
photo_path = os.path.join("img", image_filename)
|
||||
|
||||
# Проверяем существование файла изображения
|
||||
if os.path.isfile(photo_path):
|
||||
try:
|
||||
async with aiofiles.open(photo_path, "rb") as image_file:
|
||||
image_data = await image_file.read()
|
||||
buffered_photo = BufferedInputFile(image_data, filename=image_filename)
|
||||
await bot.send_photo(tg_id, buffered_photo, caption=caption, reply_markup=keyboard)
|
||||
except TelegramForbiddenError as e:
|
||||
logger.error(f"Ошибка отправки фото для пользователя {tg_id}: {e}")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки фото для пользователя {tg_id}: {e}")
|
||||
try:
|
||||
await bot.send_message(tg_id, caption, reply_markup=keyboard)
|
||||
except TelegramForbiddenError as e:
|
||||
logger.error(f"Ошибка отправки fallback-сообщения для пользователя {tg_id}: {e}")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Неизвестная ошибка при отправке fallback-сообщения для пользователя {tg_id}: {e}")
|
||||
return
|
||||
return await _send_photo_notification(bot, tg_id, photo_path, image_filename, caption, keyboard)
|
||||
else:
|
||||
logger.error(f"Файл с изображением не найден: {photo_path}")
|
||||
try:
|
||||
await bot.send_message(tg_id, caption, reply_markup=keyboard)
|
||||
except TelegramForbiddenError as e:
|
||||
logger.error(f"Ошибка отправки сообщения для пользователя {tg_id}: {e}")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Неизвестная ошибка при отправке сообщения для пользователя {tg_id}: {e}")
|
||||
return
|
||||
logger.warning(f"Файл с изображением не найден: {photo_path}")
|
||||
return await _send_text_notification(bot, tg_id, caption, keyboard)
|
||||
|
||||
|
||||
async def _send_photo_notification(
|
||||
bot: Bot,
|
||||
tg_id: int,
|
||||
photo_path: str,
|
||||
image_filename: str,
|
||||
caption: str,
|
||||
keyboard: InlineKeyboardMarkup | None = None,
|
||||
) -> bool:
|
||||
"""Отправляет уведомление с изображением."""
|
||||
try:
|
||||
async with aiofiles.open(photo_path, "rb") as image_file:
|
||||
image_data = await image_file.read()
|
||||
buffered_photo = BufferedInputFile(image_data, filename=image_filename)
|
||||
await bot.send_photo(tg_id, buffered_photo, caption=caption, reply_markup=keyboard)
|
||||
return True
|
||||
except TelegramForbiddenError:
|
||||
logger.error(f"Пользователь {tg_id} заблокировал бота")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки фото для пользователя {tg_id}: {e}")
|
||||
# Пробуем отправить текстовое сообщение как запасной вариант
|
||||
return await _send_text_notification(bot, tg_id, caption, keyboard)
|
||||
|
||||
|
||||
async def _send_text_notification(
|
||||
bot: Bot,
|
||||
tg_id: int,
|
||||
caption: str,
|
||||
keyboard: InlineKeyboardMarkup | None = None,
|
||||
) -> bool:
|
||||
"""Отправляет текстовое уведомление."""
|
||||
try:
|
||||
await bot.send_message(tg_id, caption, reply_markup=keyboard)
|
||||
return True
|
||||
except TelegramForbiddenError:
|
||||
logger.error(f"Пользователь {tg_id} заблокировал бота")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Неизвестная ошибка при отправке сообщения для пользователя {tg_id}: {e}")
|
||||
return False
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import asyncio
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import asyncpg
|
||||
import pytz
|
||||
|
||||
from aiogram import Bot, Router, types
|
||||
from aiogram.exceptions import TelegramForbiddenError
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import NOTIFY_EXTRA_DAYS, NOTIFY_INACTIVE, NOTIFY_INACTIVE_TRAFFIC, SUPPORT_CHAT_URL, TRIAL_TIME
|
||||
|
||||
from database import (
|
||||
add_notification,
|
||||
check_notification_time,
|
||||
@@ -16,6 +18,7 @@ from database import (
|
||||
from handlers.keys.key_utils import get_user_traffic
|
||||
from logger import logger
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
moscow_tz = pytz.timezone("Europe/Moscow")
|
||||
@@ -167,7 +170,7 @@ async def notify_users_no_traffic(bot: Bot, conn: asyncpg.Connection, current_ti
|
||||
continue
|
||||
|
||||
total_traffic = sum(
|
||||
value if isinstance(value, (int, float)) else 0 for value in traffic_data.get("traffic", {}).values()
|
||||
value if isinstance(value, int | float) else 0 for value in traffic_data.get("traffic", {}).values()
|
||||
)
|
||||
logger.info(f"Ключ для {email}: общий трафик: {total_traffic} ГБ")
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,6 @@
|
||||
from aiogram import F, Router
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import (
|
||||
CRYPTO_BOT_ENABLE,
|
||||
DONATIONS_ENABLE,
|
||||
@@ -13,6 +12,7 @@ from config import (
|
||||
|
||||
from .utils import edit_or_send_message
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
|
||||
+12
-11
@@ -1,7 +1,10 @@
|
||||
import html
|
||||
import os
|
||||
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.fsm.context import FSMContext
|
||||
@@ -14,7 +17,6 @@ from aiogram.types import (
|
||||
Message,
|
||||
)
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import (
|
||||
DATABASE_URL,
|
||||
INLINE_MODE,
|
||||
@@ -25,6 +27,7 @@ from config import (
|
||||
TRIAL_TIME,
|
||||
USERNAME_BOT,
|
||||
)
|
||||
|
||||
from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial
|
||||
from handlers.buttons.profile import (
|
||||
ADD_SUB,
|
||||
@@ -40,10 +43,10 @@ from handlers.buttons.profile import (
|
||||
from handlers.texts import get_referral_link, invite_message_send, profile_message_send
|
||||
from keyboards.admin.panel_kb import AdminPanelCallback
|
||||
from logger import logger
|
||||
import html
|
||||
|
||||
from .utils import edit_or_send_message
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@@ -56,7 +59,7 @@ async def process_callback_view_profile(
|
||||
):
|
||||
if isinstance(callback_query_or_message, CallbackQuery):
|
||||
chat_id = callback_query_or_message.message.chat.id
|
||||
username = html.escape(callback_query_or_message.from_user.full_name)
|
||||
username = html.escape(callback_query_or_message.from_user.full_name)
|
||||
target_message = callback_query_or_message.message
|
||||
else:
|
||||
chat_id = callback_query_or_message.chat.id
|
||||
@@ -176,14 +179,12 @@ async def view_tariffs_handler(callback_query: CallbackQuery):
|
||||
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
|
||||
|
||||
image_path = os.path.join("img", "tariffs.jpg")
|
||||
tariffs_message = "<b>🚀 Доступные тарифы VPN:</b>\n\n" + "\n".join(
|
||||
[
|
||||
f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: "
|
||||
f"{RENEWAL_PLANS[months]['price']} "
|
||||
f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей"
|
||||
for months in sorted(RENEWAL_PLANS.keys(), key=int)
|
||||
]
|
||||
)
|
||||
tariffs_message = "<b>🚀 Доступные тарифы VPN:</b>\n\n" + "\n".join([
|
||||
f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: "
|
||||
f"{RENEWAL_PLANS[months]['price']} "
|
||||
f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей"
|
||||
for months in sorted(RENEWAL_PLANS.keys(), key=int)
|
||||
])
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
|
||||
+13
-5
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiogram import F, Router
|
||||
@@ -10,8 +11,6 @@ from aiogram.types import (
|
||||
Message,
|
||||
)
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot import bot
|
||||
from config import (
|
||||
CAPTCHA_ENABLE,
|
||||
CHANNEL_EXISTS,
|
||||
@@ -21,6 +20,8 @@ from config import (
|
||||
DONATIONS_ENABLE,
|
||||
SUPPORT_CHAT_URL,
|
||||
)
|
||||
|
||||
from bot import bot
|
||||
from database import (
|
||||
add_connection,
|
||||
add_referral,
|
||||
@@ -37,6 +38,7 @@ from logger import logger
|
||||
|
||||
from .utils import edit_or_send_message
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@@ -78,7 +80,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
|
||||
)
|
||||
return
|
||||
else:
|
||||
logger.info(f"Пользователь {message.chat.id} подписан на канал (статус: {member.status}). Продолжаем работу.")
|
||||
logger.info(
|
||||
f"Пользователь {message.chat.id} подписан на канал (статус: {member.status}). Продолжаем работу."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки подписки пользователя {message.chat.id}: {e}")
|
||||
await state.update_data(start_text=text_to_process)
|
||||
@@ -259,8 +263,12 @@ async def process_start_logic(
|
||||
logger.info(f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}")
|
||||
await message.answer(f"Вы стали рефералом пользователя с ID {referrer_tg_id}")
|
||||
try:
|
||||
await bot.send_message(referrer_tg_id, f"🎉 Ваш реферал {message.chat.id} успешно зарегистрировался!")
|
||||
logger.info(f"Уведомление отправлено пользователю {referrer_tg_id} о новом реферале {message.chat.id}")
|
||||
await bot.send_message(
|
||||
referrer_tg_id, f"🎉 Ваш реферал {message.chat.id} успешно зарегистрировался!"
|
||||
)
|
||||
logger.info(
|
||||
f"Уведомление отправлено пользователю {referrer_tg_id} о новом реферале {message.chat.id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось отправить уведомление пригласившему ({referrer_tg_id}): {e}")
|
||||
|
||||
|
||||
+4
-3
@@ -7,10 +7,11 @@ import string
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
import asyncpg
|
||||
|
||||
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup, InputMediaPhoto, Message
|
||||
from config import DATABASE_URL
|
||||
|
||||
from bot import bot
|
||||
from config import DATABASE_URL
|
||||
from database import get_all_keys, get_servers
|
||||
from logger import logger
|
||||
|
||||
@@ -65,7 +66,7 @@ async def get_least_loaded_cluster() -> str:
|
||||
"""
|
||||
servers = await get_servers()
|
||||
|
||||
cluster_loads: dict[str, int] = {cluster_id: 0 for cluster_id in servers.keys()}
|
||||
cluster_loads: dict[str, int] = dict.fromkeys(servers.keys(), 0)
|
||||
|
||||
async with asyncpg.create_pool(DATABASE_URL) as pool:
|
||||
async with pool.acquire() as conn:
|
||||
@@ -199,7 +200,7 @@ async def edit_or_send_message(
|
||||
disable_web_page_preview=disable_web_page_preview,
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
await target_message.answer(
|
||||
text=text,
|
||||
reply_markup=reply_markup,
|
||||
|
||||
@@ -7,7 +7,7 @@ class AdminPanelCallback(CallbackData, prefix="admin_panel"):
|
||||
action: str
|
||||
page: int
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
if "page" not in kwargs or kwargs["page"] is None:
|
||||
kwargs["page"] = 1
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
|
||||
from aiogram.filters.callback_data import CallbackData
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import RENEWAL_PRICES
|
||||
|
||||
from keyboards.admin.panel_kb import build_admin_back_btn
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
log_folder = "logs"
|
||||
|
||||
if not os.path.exists(log_folder):
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Any, Dict, Set, cast
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import CallbackQuery, Message, TelegramObject
|
||||
|
||||
from config import ADMIN_ID
|
||||
|
||||
|
||||
@@ -15,7 +14,7 @@ class AdminMiddleware(BaseMiddleware):
|
||||
"""
|
||||
|
||||
# Кэш ID администраторов для быстрого доступа
|
||||
_admin_ids: set[int] = set(ADMIN_ID) if isinstance(ADMIN_ID, (list, tuple)) else {ADMIN_ID}
|
||||
_admin_ids: set[int] = set(ADMIN_ID) if isinstance(ADMIN_ID, list | tuple) else {ADMIN_ID}
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
|
||||
@@ -2,9 +2,9 @@ from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Optional
|
||||
|
||||
import asyncpg
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject
|
||||
|
||||
from config import DATABASE_URL
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Any, Awaitable, Callable, Dict
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Dict
|
||||
|
||||
from aiogram import BaseMiddleware, Bot
|
||||
from aiogram.types import TelegramObject, CallbackQuery
|
||||
from aiogram.types import CallbackQuery, TelegramObject
|
||||
from cachetools import TTLCache
|
||||
|
||||
|
||||
THROTTLE_TIME = 1.0
|
||||
|
||||
|
||||
@@ -13,10 +15,10 @@ class ThrottlingMiddleware(BaseMiddleware):
|
||||
self.throttle_notice_cache = TTLCache(maxsize=10_000, ttl=3.0)
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any],
|
||||
self,
|
||||
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: dict[str, Any],
|
||||
) -> Any:
|
||||
bot: Bot | None = data.get("bot", None)
|
||||
user_id = event.from_user.id if event.from_user else None
|
||||
@@ -27,7 +29,7 @@ class ThrottlingMiddleware(BaseMiddleware):
|
||||
await bot.answer_callback_query(
|
||||
callback_query_id=event.id,
|
||||
text="Слишком много запросов! Пожалуйста, подождите...",
|
||||
show_alert=False
|
||||
show_alert=False,
|
||||
)
|
||||
return None
|
||||
else:
|
||||
|
||||
+30
-1
@@ -44,13 +44,16 @@ dependencies = [
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py312"
|
||||
fix = true
|
||||
unsafe-fixes = true
|
||||
preview = true
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "I", "N", "UP", "ANN", "ASYNC", "S", "BLE", "FBT", "B", "A", "C4", "DTZ", "T10", "ISC", "ICN", "G", "PIE"]
|
||||
ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001','W291','ANN401','DTZ003','DTZ005','F401','FBT002','FBT001','FBT003','A005','E501','UP017','DTZ004','W293','ANN202','DTZ007']
|
||||
ignore = ["ANN101", "ANN102", "S101", "ANN201", "ANN001", "BLE001", "W291", "ANN401", "DTZ003", "DTZ005", "F401", "FBT002", "FBT001", "FBT003", "A005", "E501", "UP017", "DTZ004", "W293", "ANN202", "DTZ007"]
|
||||
exclude = [
|
||||
".git",
|
||||
"venv",
|
||||
@@ -61,6 +64,32 @@ exclude = [
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
skip-magic-trailing-comma = false
|
||||
line-ending = "auto"
|
||||
docstring-code-format = true
|
||||
docstring-code-line-length = 88
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
case-sensitive = true
|
||||
combine-as-imports = true
|
||||
force-wrap-aliases = true
|
||||
known-first-party = ["Solo_bot"]
|
||||
lines-after-imports = 2
|
||||
lines-between-types = 1
|
||||
|
||||
[tool.ruff.lint.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
inline-quotes = "double"
|
||||
multiline-quotes = "double"
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
[tool.ruff.lint.pycodestyle]
|
||||
max-doc-length = 120
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 10
|
||||
|
||||
[tool.darker]
|
||||
src = ["."]
|
||||
|
||||
+4
-2
@@ -1,17 +1,19 @@
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from config import ADMIN_ID, PING_TIME
|
||||
from ping3 import ping
|
||||
|
||||
from bot import bot
|
||||
from config import ADMIN_ID, PING_TIME
|
||||
from database import get_servers
|
||||
from keyboards.admin.servers_kb import AdminServerEditorCallback
|
||||
from logger import logger
|
||||
|
||||
|
||||
last_ping_times = {}
|
||||
last_notification_times = {}
|
||||
PING_SEMAPHORE = asyncio.Semaphore(3)
|
||||
@@ -32,7 +34,7 @@ async def ping_server(server_ip: str) -> bool:
|
||||
async def check_tcp_connection(host: str, port: int) -> bool:
|
||||
"""Проверяет доступность сервера через TCP (порт 443)."""
|
||||
try:
|
||||
reader, writer = await asyncio.open_connection(host, port)
|
||||
_reader, writer = await asyncio.open_connection(host, port)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user