Compare commits

..

18 Commits

Author SHA1 Message Date
ff615e2b67 Обновить .woodpecker/pipeline.yml
Some checks are pending
ci/woodpecker/push/pipeline Pipeline is pending
ci/woodpecker/manual/pipeline Pipeline is pending
2025-04-05 23:59:19 +03:00
1a93336d30 Deploy: change deployment
Some checks are pending
ci/woodpecker/push/pipeline Pipeline was successful
ci/woodpecker/manual/pipeline Pipeline is pending
2025-01-22 13:26:00 +03:00
17a1e4bb36 Feat: change deployment
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
2025-01-22 13:25:45 +03:00
d5b045984f Deploy: update woodpecker
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
ci/woodpecker/manual/pipeline Pipeline was successful
2025-01-22 12:59:58 +03:00
0faff426a7 Feat: update woodpecker
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
2025-01-22 12:59:35 +03:00
e3a15dad50 Hotfix: pg_isready
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2025-01-20 15:27:47 +03:00
970a0e6fd4 Hotfix: pg_isready healthcheck
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-01-20 15:27:31 +03:00
8420ff3f52 Deploy: Added url correcter
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-01-20 13:47:27 +03:00
c13eb62770 Feat: url correcter util
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-01-20 13:44:54 +03:00
8be7677f4b Deploy: Get users command
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2025-01-17 14:25:47 +03:00
0fa538d079 Feat: get users command
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-01-17 14:24:41 +03:00
fed548e9e8 Feat: admin help command 2025-01-17 14:10:02 +03:00
3ad4c2ea3e First hotfix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2025-01-17 14:04:35 +03:00
e3c7f96693 Merge branch 'hotfix-start-command' 2025-01-17 13:52:33 +03:00
e82c392fc8 Hotfix: user deleting tg_id 2025-01-17 13:51:57 +03:00
0a844f7369 Deploy: First ever deploy
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2025-01-17 13:20:41 +03:00
926774424e Feat: added notifier
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-01-17 13:19:13 +03:00
0d1dd4362d Feat: fixed notifications 2025-01-16 16:51:26 +03:00
14 changed files with 153 additions and 37 deletions

View File

@@ -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

View File

@@ -1,6 +1,3 @@
when:
- branch: [master, deploy]
event: [push, manual]
steps: steps:
- name: build - name: build
image: docker:latest image: docker:latest
@@ -13,7 +10,8 @@ steps:
ENV_FILE: ENV_FILE:
from_secret: ENV_FILE from_secret: ENV_FILE
when: when:
- event: [push, manual] - branch: [deploy, master]
event: [push, manual]
- name: deploy - name: deploy
image: docker:latest image: docker:latest
commands: commands:
@@ -24,6 +22,7 @@ steps:
environment: environment:
ENV_FILE: ENV_FILE:
from_secret: ENV_FILE from_secret: ENV_FILE
depends_on: [build] depends_on: build
when: when:
- event: manual - branch: deploy
event: manual

View File

@@ -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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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:

View File

@@ -0,0 +1 @@
from nwxraybot.fsm.broadcast import BroadcastStates

View File

@@ -0,0 +1,6 @@
from aiogram.fsm.state import State, StatesGroup
class BroadcastStates(StatesGroup):
waiting_for_message = State()
confirming_message = State()

View File

@@ -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('Рассылка отменена.')

View File

@@ -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()

View File

@@ -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:

View File

@@ -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()

View File

@@ -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()

View File

@@ -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