Compare commits
12 Commits
7d50cf95cf
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 17a1e4bb36 | |||
| 0faff426a7 | |||
| 970a0e6fd4 | |||
| c13eb62770 | |||
| 0fa538d079 | |||
| fed548e9e8 | |||
| 3ad4c2ea3e | |||
| e3c7f96693 | |||
| e82c392fc8 | |||
| 0a844f7369 | |||
| 926774424e | |||
| 0d1dd4362d |
@@ -5,5 +5,6 @@ POSTGRES_PASSWORD=postgres
|
|||||||
POSTGRES_DB=postgres
|
POSTGRES_DB=postgres
|
||||||
POSTGRES_HOST=postgres
|
POSTGRES_HOST=postgres
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
|
DOMAIN_NAME=localhost
|
||||||
|
|
||||||
COMPOSE_PROJECT_NAME=nwxraybot
|
COMPOSE_PROJECT_NAME=nwxraybot
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
when:
|
when:
|
||||||
- branch: [master, deploy]
|
- branch: [master, deploy]
|
||||||
event: [push, manual]
|
|
||||||
steps:
|
steps:
|
||||||
- name: build
|
- name: build
|
||||||
image: docker:latest
|
image: docker:latest
|
||||||
@@ -24,6 +23,7 @@ steps:
|
|||||||
environment:
|
environment:
|
||||||
ENV_FILE:
|
ENV_FILE:
|
||||||
from_secret: ENV_FILE
|
from_secret: ENV_FILE
|
||||||
depends_on: [build]
|
depends_on: build
|
||||||
when:
|
when:
|
||||||
|
- branch: deploy
|
||||||
- event: manual
|
- event: manual
|
||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${POSTGRES_PORT}:5432"
|
- "127.0.0.1:${POSTGRES_PORT}:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "pg_isready"]
|
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
9
main.py
9
main.py
@@ -8,10 +8,6 @@ from nwxraybot import NwXrayBot, Settings
|
|||||||
from nwxraybot.handlers import *
|
from nwxraybot.handlers import *
|
||||||
from nwxraybot.models import User
|
from nwxraybot.models import User
|
||||||
|
|
||||||
|
|
||||||
async def main(bot: NwXrayBot, skip_updates: bool = True) -> None:
|
|
||||||
await asyncio.create_task(bot.start())
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
config = Settings() # Load config from .env
|
config = Settings() # Load config from .env
|
||||||
logging.basicConfig(level=logging.DEBUG if config.debug else logging.INFO)
|
logging.basicConfig(level=logging.DEBUG if config.debug else logging.INFO)
|
||||||
@@ -29,6 +25,5 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# Start bot
|
# Start bot
|
||||||
bot = NwXrayBot(config.bot_token.get_secret_value())
|
bot = NwXrayBot(config.bot_token.get_secret_value())
|
||||||
bot.include_routers(HelloHandler(), MenuHandler(), AdminHandler())
|
bot.include_routers(HelloHandler(), MenuHandler(), AdminHandler(bot.bot))
|
||||||
|
loop.run_until_complete(bot.start(skip_updates=True))
|
||||||
asyncio.run(main(bot))
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
from nwxraybot.bot import NwXrayBot
|
from nwxraybot.bot import NwXrayBot
|
||||||
from nwxraybot.config import Settings
|
from nwxraybot.config import Settings
|
||||||
from nwxraybot.utils import get_code, get_subscription_info
|
from nwxraybot.utils import (get_code, get_correct_user_url,
|
||||||
|
get_subscription_info)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class Settings(BaseSettings):
|
|||||||
postgres_db: str = Field('db', env="POSTGRES_DB")
|
postgres_db: str = Field('db', env="POSTGRES_DB")
|
||||||
postgres_host: str = Field('localhost', env="POSTGRES_HOST")
|
postgres_host: str = Field('localhost', env="POSTGRES_HOST")
|
||||||
postgres_port: int = Field(5432, env="POSTGRES_PORT")
|
postgres_port: int = Field(5432, env="POSTGRES_PORT")
|
||||||
|
domain_name: str = Field('localhost', env="DOMAIN_NAME")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def postgres_url(self) -> str:
|
def postgres_url(self) -> str:
|
||||||
|
|||||||
1
nwxraybot/fsm/__init__.py
Normal file
1
nwxraybot/fsm/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from nwxraybot.fsm.broadcast import BroadcastStates
|
||||||
6
nwxraybot/fsm/broadcast.py
Normal file
6
nwxraybot/fsm/broadcast.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
|
||||||
|
|
||||||
|
class BroadcastStates(StatesGroup):
|
||||||
|
waiting_for_message = State()
|
||||||
|
confirming_message = State()
|
||||||
@@ -1,22 +1,41 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from aiogram import Bot, F
|
||||||
from aiogram.enums import ParseMode
|
from aiogram.enums import ParseMode
|
||||||
from aiogram.filters import Command
|
from aiogram.filters import Command
|
||||||
from aiogram.types import Message
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import (CallbackQuery, InlineKeyboardButton,
|
||||||
|
InlineKeyboardMarkup, Message)
|
||||||
|
|
||||||
from nwxraybot import get_code
|
from nwxraybot import get_code
|
||||||
|
from nwxraybot.fsm import BroadcastStates
|
||||||
from nwxraybot.meta import Handler
|
from nwxraybot.meta import Handler
|
||||||
from nwxraybot.middlewares import AdminMiddleware
|
from nwxraybot.middlewares import AdminMiddleware
|
||||||
from nwxraybot.models import User
|
from nwxraybot.models import User
|
||||||
|
from nwxraybot.utils import get_correct_user_url
|
||||||
|
|
||||||
|
|
||||||
class AdminHandler(Handler):
|
class AdminHandler(Handler):
|
||||||
def __init__(self) -> None:
|
def __init__(self, bot: Optional[Bot] = None) -> None:
|
||||||
super().__init__()
|
super().__init__(bot)
|
||||||
|
|
||||||
self.router.message.middleware(AdminMiddleware())
|
self.router.message.middleware(AdminMiddleware())
|
||||||
|
|
||||||
|
help_text = """Список команда администратора:
|
||||||
|
`/adduser name url [01.01.1970 00:00]` - добавить пользователя
|
||||||
|
`/updateuser name 01.01.1970 00:00` - обновить информацию о пользователе
|
||||||
|
`/broadcast` - рассылка (пошагово)
|
||||||
|
`/get_users` - список пользователей
|
||||||
|
"""
|
||||||
|
|
||||||
|
@self.router.message(Command('ahelp'))
|
||||||
|
async def help(message: Message):
|
||||||
|
await message.reply(help_text, parse_mode=ParseMode.MARKDOWN)
|
||||||
|
|
||||||
@self.router.message(Command('adduser'))
|
@self.router.message(Command('adduser'))
|
||||||
async def add_user(message: Message):
|
async def add_user(message: Message):
|
||||||
mask = r"^(?P<name>[a-zA-Z0-9]+)\s(?P<url>vless://[^\s]+)($|\s(?P<date>[0-9]{2}\.[0-9]{2}\.[0-9]{4})\s(?P<time>[0-9]{2}\:[0-9]{2})$)"
|
mask = r"^(?P<name>[a-zA-Z0-9]+)\s(?P<url>vless://[^\s]+)($|\s(?P<date>[0-9]{2}\.[0-9]{2}\.[0-9]{4})\s(?P<time>[0-9]{2}\:[0-9]{2})$)"
|
||||||
@@ -26,13 +45,15 @@ class AdminHandler(Handler):
|
|||||||
await message.reply('Вы ввели команду в неверном формате. Вводите в формате:\n``` /adduser name vless://.... 01.01.1970 00:00```', parse_mode=ParseMode.MARKDOWN)
|
await message.reply('Вы ввели команду в неверном формате. Вводите в формате:\n``` /adduser name vless://.... 01.01.1970 00:00```', parse_mode=ParseMode.MARKDOWN)
|
||||||
return
|
return
|
||||||
user_dict = match.groupdict()
|
user_dict = match.groupdict()
|
||||||
|
url = user_dict['url']
|
||||||
|
url = get_correct_user_url(url)
|
||||||
date = None
|
date = None
|
||||||
if user_dict['date']:
|
if user_dict['date']:
|
||||||
date = datetime.strptime(f"{user_dict['date']} {
|
date = datetime.strptime(f"{user_dict['date']} {
|
||||||
user_dict['time']}", "%d.%m.%Y %H:%M")
|
user_dict['time']}", "%d.%m.%Y %H:%M")
|
||||||
code = get_code()
|
code = get_code()
|
||||||
new_user = User(
|
new_user = User(
|
||||||
name=user_dict['name'], url=user_dict['url'], time=date, code=code)
|
name=user_dict['name'], url=url, time=date, code=code)
|
||||||
new_user.save()
|
new_user.save()
|
||||||
await message.answer(f'Пользователь создан. Вот его ссылка для доступа:\n`https://t.me/nwproxybot?start={code}`', parse_mode=ParseMode.MARKDOWN)
|
await message.answer(f'Пользователь создан. Вот его ссылка для доступа:\n`https://t.me/nwproxybot?start={code}`', parse_mode=ParseMode.MARKDOWN)
|
||||||
|
|
||||||
@@ -51,3 +72,64 @@ class AdminHandler(Handler):
|
|||||||
User.name == user_dict['name'])
|
User.name == user_dict['name'])
|
||||||
query.execute()
|
query.execute()
|
||||||
await message.answer('Информация о пользователе обновлена.')
|
await message.answer('Информация о пользователе обновлена.')
|
||||||
|
|
||||||
|
@self.router.message(Command('get_users'))
|
||||||
|
async def get_users(message: Message):
|
||||||
|
def get_user_info(user: User) -> str:
|
||||||
|
date_str = "" if user.time is None else f'До: {
|
||||||
|
user.time.strftime("%d.%m.%Y %H:%M")} МСК\n'
|
||||||
|
return f"Информация о пользователе `{user.name}`:\n{date_str}Ссылка: `{user.url}`\ncode: `{user.code if user.code else 'None'}`\n\n"
|
||||||
|
|
||||||
|
users = User.select()
|
||||||
|
res = ""
|
||||||
|
for user in users:
|
||||||
|
res += get_user_info(user)
|
||||||
|
try:
|
||||||
|
await message.answer(res, parse_mode=ParseMode.MARKDOWN)
|
||||||
|
except Exception as e:
|
||||||
|
await message.answer(f"Error while getting users: {e}")
|
||||||
|
logging.error(f"Error while getting user {
|
||||||
|
user.telegram_id}: {e}")
|
||||||
|
|
||||||
|
@self.router.message(Command('broadcast'))
|
||||||
|
async def start_broadcast(message: Message, state: FSMContext):
|
||||||
|
await message.answer('Отправьте для рассылки')
|
||||||
|
await state.set_state(BroadcastStates.waiting_for_message)
|
||||||
|
|
||||||
|
@self.router.message(BroadcastStates.waiting_for_message)
|
||||||
|
async def broadcast_message(message: Message, state: FSMContext):
|
||||||
|
if message.media_group_id is not None:
|
||||||
|
await state.clear()
|
||||||
|
await message.answer('Пожалуйста, не отправляйте сообщение с несколькими изображениями.')
|
||||||
|
return
|
||||||
|
await state.set_state(BroadcastStates.confirming_message)
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(
|
||||||
|
text="Подтвердить", callback_data='confirm_broadcast')],
|
||||||
|
[InlineKeyboardButton(
|
||||||
|
text="Отменить", callback_data='cancel_broadcast')]
|
||||||
|
])
|
||||||
|
await state.update_data(message_id=message.message_id)
|
||||||
|
await message.answer("Подтвердите отправку сообщения", reply_markup=keyboard)
|
||||||
|
|
||||||
|
@self.router.callback_query(F.data == 'confirm_broadcast', BroadcastStates.confirming_message)
|
||||||
|
async def confirm_broadcast(callback: CallbackQuery, state: FSMContext):
|
||||||
|
await callback.answer()
|
||||||
|
data = await state.get_data()
|
||||||
|
message_id = data.get('message_id')
|
||||||
|
users = User.select().where((User.telegram_id != None))
|
||||||
|
semaphore = asyncio.Semaphore(10)
|
||||||
|
for user in users:
|
||||||
|
async with semaphore:
|
||||||
|
try:
|
||||||
|
await self.bot.copy_message(user.telegram_id, callback.from_user.id, message_id)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error while broadcasting message to user {
|
||||||
|
user.telegram_id}: {e}")
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
@self.router.callback_query(F.data == 'cancel_broadcast', BroadcastStates.confirming_message)
|
||||||
|
async def cancel_broadcast(callback: CallbackQuery, state: FSMContext):
|
||||||
|
await callback.answer()
|
||||||
|
await state.clear()
|
||||||
|
await callback.message.answer('Рассылка отменена.')
|
||||||
|
|||||||
@@ -30,14 +30,20 @@ class HelloHandler(Handler):
|
|||||||
user: Optional[User] = None
|
user: Optional[User] = None
|
||||||
if len(data) == 2:
|
if len(data) == 2:
|
||||||
code = data[1]
|
code = data[1]
|
||||||
query = User.update(telegram_id=None).where(
|
user = User.select().where(
|
||||||
User.telegram_id == message.from_user.id)
|
User.telegram_id == message.from_user.id).first()
|
||||||
query.execute()
|
if user is not None:
|
||||||
|
await message.answer(f"Приветствуем в боте NwXray! Здесь вы сможете получить информацию о своем подключении к NwXray.\n\n{get_subscription_info(message.from_user.id)}",
|
||||||
|
reply_markup=self.__non_admin_main_menu(), parse_mode=ParseMode.MARKDOWN)
|
||||||
|
return
|
||||||
user = User.select().where(
|
user = User.select().where(
|
||||||
User.code == code).first()
|
User.code == code).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
await message.answer('Пользователь не найден, обратитесь к администратору за ссылкой!')
|
await message.answer('Пользователь не найден, обратитесь к администратору за ссылкой!')
|
||||||
return
|
return
|
||||||
|
query = User.update(telegram_id=None).where(
|
||||||
|
User.telegram_id == message.from_user.id)
|
||||||
|
query.execute()
|
||||||
user.telegram_id = message.from_user.id
|
user.telegram_id = message.from_user.id
|
||||||
user.code = ''
|
user.code = ''
|
||||||
user.save()
|
user.save()
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ class Handler:
|
|||||||
|
|
||||||
def __init__(self, bot: Optional[Bot] = None) -> None:
|
def __init__(self, bot: Optional[Bot] = None) -> None:
|
||||||
if bot:
|
if bot:
|
||||||
assert isinstance(bot.bot, Optional[Bot])
|
assert isinstance(bot, Bot)
|
||||||
self.bot = bot.bot
|
self.bot = bot
|
||||||
self.router = Router()
|
self.router = Router()
|
||||||
|
|
||||||
def __call__(self) -> Router:
|
def __call__(self) -> Router:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class UserMiddleware(BaseMiddleware):
|
|||||||
async def __call__(self, handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], event: Message, data: Dict[str, Any]) -> Any:
|
async def __call__(self, handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], event: Message, data: Dict[str, Any]) -> Any:
|
||||||
if event.chat.type != ChatType.PRIVATE:
|
if event.chat.type != ChatType.PRIVATE:
|
||||||
return None
|
return None
|
||||||
if event.text.startswith('/start'):
|
if event.text and event.text.startswith('/start'):
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
user: Optional[User] = User.select().where(
|
user: Optional[User] = User.select().where(
|
||||||
User.telegram_id == event.from_user.id).first()
|
User.telegram_id == event.from_user.id).first()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
@@ -8,21 +7,29 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|||||||
from nwxraybot.models import User
|
from nwxraybot.models import User
|
||||||
|
|
||||||
|
|
||||||
# TODO: This shit is not working
|
|
||||||
async def send_subscription_notification(bot: Bot, telegram_id: int) -> None:
|
|
||||||
logging.debug(f"Sending subscription notification to {telegram_id}")
|
|
||||||
|
|
||||||
|
|
||||||
async def check_subscription_status(bot: Bot):
|
async def check_subscription_status(bot: Bot):
|
||||||
logging.debug('Running notifier task')
|
|
||||||
await asyncio.sleep(1)
|
# Check users subscription status
|
||||||
for user in User.select():
|
now_time = datetime.datetime.now()
|
||||||
if user.telegram_id != '':
|
for user in User.select().where((User.time > now_time) & ((User.time - now_time <= datetime.timedelta(days=7))) & (User.telegram_id != '')):
|
||||||
await bot.send_message(user.telegram_id, 'Your subscription is active')
|
text = ""
|
||||||
|
delta = user.time - now_time
|
||||||
|
print(delta)
|
||||||
|
if delta.days <= 1:
|
||||||
|
text = "Скоро истекает срок действия вашей подписки. Не теряйте доступ к NwaifuVPN — продлите подписку прямо сейчас!"
|
||||||
|
elif delta.days <= 3:
|
||||||
|
text = "До окончания вашей подписки осталось менее 3-х дней. Продлите, чтобы избежать отключения."
|
||||||
|
else:
|
||||||
|
text = "Ваша подписка заканчивается через менее чем 7 дней. Продлите её, чтобы избежать отключения."
|
||||||
|
await bot.send_message(user.telegram_id, text)
|
||||||
|
|
||||||
|
# Check non-paying users
|
||||||
|
for user in User.select().where((User.time < now_time) & (now_time - User.time < datetime.timedelta(days=1)) & (User.telegram_id != '')):
|
||||||
|
await bot.send_message(user.telegram_id, "Ваша подписка истекла. Чтобы восстановить доступ к NwaifuVPN, продлите подписку уже сегодня")
|
||||||
|
|
||||||
|
|
||||||
def setup_subscription_notifier(bot: Bot) -> None:
|
def setup_subscription_notifier(bot: Bot) -> None:
|
||||||
scheduler = AsyncIOScheduler(event_loop=asyncio.get_event_loop())
|
scheduler = AsyncIOScheduler(event_loop=asyncio.get_event_loop())
|
||||||
scheduler.add_job(check_subscription_status,
|
scheduler.add_job(check_subscription_status,
|
||||||
'interval', seconds=10, args=[bot])
|
'cron', hour=10, minute=0, args=[bot])
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import logging
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
|
from nwxraybot import Settings
|
||||||
from nwxraybot.models import User
|
from nwxraybot.models import User
|
||||||
|
|
||||||
|
config = Settings()
|
||||||
|
|
||||||
|
|
||||||
def get_subscription_info(telegram_id: str) -> str:
|
def get_subscription_info(telegram_id: str) -> str:
|
||||||
user: User = User.select().where(User.telegram_id == telegram_id).first()
|
user: User = User.select().where(User.telegram_id == telegram_id).first()
|
||||||
@@ -20,3 +24,16 @@ def get_subscription_info(telegram_id: str) -> str:
|
|||||||
|
|
||||||
def get_code(length: int = 10) -> str:
|
def get_code(length: int = 10) -> str:
|
||||||
return token_urlsafe(length)[:length]
|
return token_urlsafe(length)[:length]
|
||||||
|
|
||||||
|
|
||||||
|
def get_correct_user_url(url: str) -> str:
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
query = parse_qs(parsed_url.query)
|
||||||
|
query['fp'] = 'chrome'
|
||||||
|
query['alpn'] = 'h2,h3'
|
||||||
|
query['packetEncoding'] = 'xudp'
|
||||||
|
query['security'] = 'tls'
|
||||||
|
new_query = urlencode(query, doseq=True)
|
||||||
|
new_url = urlunparse((parsed_url.scheme, parsed_url.netloc.replace("127.0.0.1:1234", f"{
|
||||||
|
config.domain_name}:443"), parsed_url.path, parsed_url.params, new_query, parsed_url.fragment))
|
||||||
|
return new_url
|
||||||
|
|||||||
Reference in New Issue
Block a user