Compare commits

...

30 Commits

Author SHA1 Message Date
17a1e4bb36 Feat: change deployment
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
2025-01-22 13:25:45 +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
970a0e6fd4 Hotfix: pg_isready healthcheck
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-01-20 15:27:31 +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
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
7d50cf95cf Feat: started working on the sub notification 2025-01-13 15:50:45 +03:00
b5a893e5aa Feat: some fixes 2025-01-13 15:18:48 +03:00
9ee8e43fb4 Feat: fixed start command 2025-01-13 11:49:59 +03:00
af837b54be Feat: disabled menu command 2025-01-05 21:02:00 +03:00
9f98d9502e Feat: update user command 2025-01-05 20:58:25 +03:00
3962c57dde Feat: add user command 2025-01-05 20:44:51 +03:00
34308f2cac Feat: started main user interface 2025-01-02 22:01:29 +03:00
7093f7452d Feat: added user middleware 2025-01-02 22:01:05 +03:00
e73bc6569b Feat: removed unnecessary bot 2025-01-02 22:00:44 +03:00
5d6310e5a4 Feat: started menu command and admin field 2024-12-28 11:50:40 +03:00
a5d708383a Feat: added compose project name to .env
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-28 09:56:06 +03:00
b909c77180 Feat: move from stack to compose
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-28 09:50:57 +03:00
1f6ed3d09c Feat: .env example
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-27 21:14:04 +03:00
f0ac459dea Feat: rules
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-27 21:09:16 +03:00
f288b99123 Feat: removed PR
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-27 21:06:25 +03:00
a718c15be3 Feat: fixed conditions
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-27 20:54:55 +03:00
f3519b8de6 Feat: some conditions
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-27 20:50:59 +03:00
eecc7b3920 Feat: some renamings
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-12-27 20:41:54 +03:00
26 changed files with 466 additions and 59 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
__pycache__

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
BOT_TOKEN=token
DEBUG=1
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
DOMAIN_NAME=localhost
COMPOSE_PROJECT_NAME=nwxraybot

View File

@@ -1,25 +1,29 @@
when:
- event: push
branch: deploy
- branch: [master, deploy]
steps:
- name: build
image: docker:latest
commands:
- echo "$ENV_FILE" > .env
- docker build -t aboba .
- docker build -t nwxraybot:latest .
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
ENV_FILE:
from_secret: ENV_FILE
when:
- event: [push, manual]
- name: deploy
image: docker:latest
commands:
- echo "$ENV_FILE" > .env
- sh run.sh
- docker compose up -d
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
ENV_FILE:
from_secret: ENV_FILE
from_secret: ENV_FILE
depends_on: build
when:
- branch: deploy
- event: manual

View File

@@ -1,29 +1,30 @@
services:
bot:
image: aboba:latest
deploy:
replicas: 1
restart_policy:
condition: on-failure
image: nwxraybot:latest
restart: always
container_name: nwxraybot_bot
build:
context: .
dockerfile: Dockerfile
networks:
- bot_network
depends_on:
- db
db:
condition: service_healthy
restart: true
db:
image: postgres:17-alpine
deploy:
replicas: 1
restart_policy:
condition: on-failure
container_name: nwxraybot_db
restart: always
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-postgres}
ports:
- 5432
- "127.0.0.1:${POSTGRES_PORT}:5432"
healthcheck:
test: ["CMD", "pg_isready"]
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 10
@@ -35,7 +36,6 @@ services:
networks:
bot_network:
driver: overlay
volumes:
postgres_data:

10
main.py
View File

@@ -5,7 +5,7 @@ from sys import exit
import uvloop
from nwxraybot import NwXrayBot, Settings
from nwxraybot.handlers import HelloHandler
from nwxraybot.handlers import *
from nwxraybot.models import User
if __name__ == "__main__":
@@ -19,7 +19,11 @@ if __name__ == "__main__":
User.create_table()
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)
# Start bot
bot = NwXrayBot(config.bot_token.get_secret_value())
bot.include_routers(HelloHandler(bot)())
uvloop.run(bot.start(skip_updates=True))
bot.include_routers(HelloHandler(), MenuHandler(), AdminHandler(bot.bot))
loop.run_until_complete(bot.start(skip_updates=True))

View File

@@ -1,2 +1,4 @@
from nwxraybot.bot import NwXrayBot
from nwxraybot.config import Settings
from nwxraybot.utils import (get_code, get_correct_user_url,
get_subscription_info)

View File

@@ -1,14 +1,23 @@
from aiogram import Bot, Dispatcher, Router
from aiogram.utils.callback_answer import CallbackAnswerMiddleware
from nwxraybot.middlewares import UserMiddleware
from nwxraybot.notifiers import setup_subscription_notifier
from .meta import Handler
class NwXrayBot:
def __init__(self, token: str) -> None:
self.bot = Bot(token=token)
self.dp = Dispatcher()
self.dp.message.middleware(UserMiddleware())
self.dp.message.middleware(CallbackAnswerMiddleware())
setup_subscription_notifier(self.bot)
def include_routers(self, *routers: Router):
def include_routers(self, *routers: Handler):
for router in routers:
self.dp.include_router(router)
self.dp.include_router(router())
async def start(self, skip_updates: bool = False) -> None:
await self.dp.start_polling(self.bot, skip_updates=skip_updates)

View File

@@ -1,5 +1,5 @@
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
@@ -10,6 +10,7 @@ class Settings(BaseSettings):
postgres_db: str = Field('db', env="POSTGRES_DB")
postgres_host: str = Field('localhost', env="POSTGRES_HOST")
postgres_port: int = Field(5432, env="POSTGRES_PORT")
domain_name: str = Field('localhost', env="DOMAIN_NAME")
@property
def postgres_url(self) -> str:
@@ -19,3 +20,4 @@ class Settings(BaseSettings):
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
extra = "allow"

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 +1,3 @@
from nwxraybot.handlers.admin import AdminHandler
from nwxraybot.handlers.hello import HelloHandler
from nwxraybot.handlers.menu import MenuHandler

135
nwxraybot/handlers/admin.py Normal file
View File

@@ -0,0 +1,135 @@
import asyncio
import logging
import re
from datetime import datetime
from typing import Optional
from aiogram import Bot, F
from aiogram.enums import ParseMode
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import (CallbackQuery, InlineKeyboardButton,
InlineKeyboardMarkup, Message)
from nwxraybot import get_code
from nwxraybot.fsm import BroadcastStates
from nwxraybot.meta import Handler
from nwxraybot.middlewares import AdminMiddleware
from nwxraybot.models import User
from nwxraybot.utils import get_correct_user_url
class AdminHandler(Handler):
def __init__(self, bot: Optional[Bot] = None) -> None:
super().__init__(bot)
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'))
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})$)"
text = message.text.replace('/adduser ', '')
match = re.match(mask, text)
if match is None:
await message.reply('Вы ввели команду в неверном формате. Вводите в формате:\n``` /adduser name vless://.... 01.01.1970 00:00```', parse_mode=ParseMode.MARKDOWN)
return
user_dict = match.groupdict()
url = user_dict['url']
url = get_correct_user_url(url)
date = None
if user_dict['date']:
date = datetime.strptime(f"{user_dict['date']} {
user_dict['time']}", "%d.%m.%Y %H:%M")
code = get_code()
new_user = User(
name=user_dict['name'], url=url, time=date, code=code)
new_user.save()
await message.answer(f'Пользователь создан. Вот его ссылка для доступа:\n`https://t.me/nwproxybot?start={code}`', parse_mode=ParseMode.MARKDOWN)
@self.router.message(Command('updateuser'))
async def update_user(message: Message):
mask = r"^(?P<name>[a-zA-Z0-9]+)\s(?P<date>[0-9]{2}\.[0-9]{2}\.[0-9]{4})\s(?P<time>[0-9]{2}\:[0-9]{2})$"
text = message.text.replace('/updateuser ', '')
match = re.match(mask, text)
if match is None:
await message.reply('Вы ввели команду в неверном формате. Вводите в формате:\n``` /updateuser name 01.01.1970 00:00```', parse_mode=ParseMode.MARKDOWN)
return
user_dict = match.groupdict()
date = datetime.strptime(f"{user_dict['date']} {
user_dict['time']}", "%d.%m.%Y %H:%M")
query = User.update(time=date).where(
User.name == user_dict['name'])
query.execute()
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

@@ -1,13 +1,65 @@
from aiogram import types
from aiogram.filters import Command
from typing import Optional
from aiogram import F, types
from aiogram.enums import ParseMode
from aiogram.filters import Command
from aiogram.types import (CallbackQuery, InlineKeyboardButton,
InlineKeyboardMarkup)
from nwxraybot import get_subscription_info
from nwxraybot.meta import Handler
from nwxraybot.models import User
class HelloHandler(Handler):
def __init__(self, bot) -> None:
super().__init__(bot)
def __non_admin_main_menu(self):
markup = [[
InlineKeyboardButton(text="Обновить данные🔄",
callback_data='update')
]]
return InlineKeyboardMarkup(
inline_keyboard=markup
)
def __init__(self) -> None:
super().__init__()
@self.router.message(Command("start"))
async def hello(message: types.Message):
await message.reply("Приветствуем в боте NwXray! Здесь вы сможете получить информацию о своем подключении к NwXray")
data = message.text.split()
user: Optional[User] = None
if len(data) == 2:
code = data[1]
user = User.select().where(
User.telegram_id == message.from_user.id).first()
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.code == code).first()
if user is None:
await message.answer('Пользователь не найден, обратитесь к администратору за ссылкой!')
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.code = ''
user.save()
user = User.select().where(User.telegram_id == message.from_user.id).first()
if user is None:
await message.answer('Пользователь не найден, обратитесь к администратору за ссылкой!')
return
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)
@self.router.callback_query(F.data == 'update')
async def update_data(callback: CallbackQuery):
text = get_subscription_info(callback.from_user.id)
try:
await callback.message.edit_text(get_subscription_info(callback.from_user.id),
reply_markup=self.__non_admin_main_menu(), parse_mode=ParseMode.MARKDOWN)
except:
await callback.answer('Обновлений нет', show_alert=True)
return

View File

@@ -0,0 +1,20 @@
from aiogram import types
from aiogram.enums import ParseMode
from aiogram.filters import Command
from nwxraybot.meta import Handler
from nwxraybot.models import User
class MenuHandler(Handler):
def __init__(self) -> None:
super().__init__()
# TODO: Temporary disabled
# @self.router.message(Command('menu'))
async def menu(message: types.Message) -> None:
user: User = User.select().where(User.id == message.from_user.id).first()
if user:
await message.reply(f"Ваша ссылка:\n```{user.url}```", parse_mode=ParseMode.MARKDOWN_V2)
else:
await message.reply("Пользователь не найден, обратитесь к админу!")

View File

@@ -1,15 +1,16 @@
from aiogram import Router
from typing import Optional
from nwxraybot import NwXrayBot
from aiogram import Bot, Router
class Handler:
bot: NwXrayBot
bot: Optional[Bot]
router: Router
def __init__(self, bot: NwXrayBot) -> None:
assert isinstance(bot, NwXrayBot)
self.bot = bot
def __init__(self, bot: Optional[Bot] = None) -> None:
if bot:
assert isinstance(bot, Bot)
self.bot = bot
self.router = Router()
def __call__(self) -> Router:

View File

@@ -0,0 +1,2 @@
from nwxraybot.middlewares.admin import AdminMiddleware
from nwxraybot.middlewares.user import UserMiddleware

View File

@@ -0,0 +1,19 @@
from typing import Any, Awaitable, Callable, Dict, Optional
from aiogram import BaseMiddleware
from aiogram.types import Message
from nwxraybot.models import User
class AdminMiddleware(BaseMiddleware):
def __init__(self) -> None:
pass
async def __call__(self, handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], event: Message, data: Dict[str, Any]) -> Any:
user: User = User.select().where(
User.telegram_id == event.from_user.id).first()
if user is None or not user.admin:
await event.reply('Вы не обладаете правами администратора для доступа к данной команде.')
return None
return await handler(event, data)

View File

@@ -0,0 +1,24 @@
from typing import Any, Awaitable, Callable, Dict, Optional
from aiogram import BaseMiddleware
from aiogram.enums import ChatType
from aiogram.types import Message
from nwxraybot.models import User
class UserMiddleware(BaseMiddleware):
def __init__(self) -> None:
pass
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:
return None
if event.text and event.text.startswith('/start'):
return await handler(event, data)
user: Optional[User] = User.select().where(
User.telegram_id == event.from_user.id).first()
if user is None:
await event.answer("Вы не зарегистрированы в системе, обратитесь к админам за доступом!")
return None
return await handler(event, data)

View File

@@ -12,9 +12,9 @@ class User(Model):
name = CharField()
url = CharField()
time = DateTimeField(null=True)
admin = BooleanField(default=False)
telegram_id = CharField(null=True)
code = CharField()
class Meta:
database = db
User.create_table()

View File

@@ -0,0 +1 @@
from nwxraybot.notifiers.subscription import setup_subscription_notifier

View File

@@ -0,0 +1,35 @@
import asyncio
import datetime
from aiogram import Bot
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from nwxraybot.models import User
async def check_subscription_status(bot: Bot):
# Check users subscription status
now_time = datetime.datetime.now()
for user in User.select().where((User.time > now_time) & ((User.time - now_time <= datetime.timedelta(days=7))) & (User.telegram_id != '')):
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:
scheduler = AsyncIOScheduler(event_loop=asyncio.get_event_loop())
scheduler.add_job(check_subscription_status,
'cron', hour=10, minute=0, args=[bot])
scheduler.start()

39
nwxraybot/utils.py Normal file
View File

@@ -0,0 +1,39 @@
import logging
from datetime import datetime
from secrets import token_urlsafe
from typing import Optional
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
from nwxraybot import Settings
from nwxraybot.models import User
config = Settings()
def get_subscription_info(telegram_id: str) -> str:
user: User = User.select().where(User.telegram_id == telegram_id).first()
if user is None:
logging.error("[get_subscription_info]: User is not found")
return "Ошибка!"
date: Optional[datetime] = user.time
date_str = "" if date is None else f'До: {
date.strftime("%d.%m.%Y %H:%M")} МСК\n'
res = f"Информация о подписке:\n{date_str}Ссылка: `{user.url}`"
return res
def get_code(length: int = 10) -> str:
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

59
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
[[package]]
name = "aiofiles"
@@ -185,6 +185,33 @@ files = [
{file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"},
]
[[package]]
name = "apscheduler"
version = "3.11.0"
description = "In-process task scheduler with Cron-like capabilities"
optional = false
python-versions = ">=3.8"
files = [
{file = "APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da"},
{file = "apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133"},
]
[package.dependencies]
tzlocal = ">=3.0"
[package.extras]
doc = ["packaging", "sphinx", "sphinx-rtd-theme (>=1.3.0)"]
etcd = ["etcd3", "protobuf (<=3.21.0)"]
gevent = ["gevent"]
mongodb = ["pymongo (>=3.0)"]
redis = ["redis (>=3.0)"]
rethinkdb = ["rethinkdb (>=2.4.0)"]
sqlalchemy = ["sqlalchemy (>=1.4)"]
test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6", "anyio (>=4.5.2)", "gevent", "pytest", "pytz", "twisted"]
tornado = ["tornado (>=4.3)"]
twisted = ["twisted"]
zookeeper = ["kazoo"]
[[package]]
name = "attrs"
version = "24.3.0"
@@ -882,6 +909,34 @@ files = [
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "tzdata"
version = "2024.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
{file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"},
{file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"},
]
[[package]]
name = "tzlocal"
version = "5.2"
description = "tzinfo object for the local timezone"
optional = false
python-versions = ">=3.8"
files = [
{file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"},
{file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"},
]
[package.dependencies]
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
[[package]]
name = "uvloop"
version = "0.21.0"
@@ -1085,4 +1140,4 @@ propcache = ">=0.2.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.13"
content-hash = "9b455b4a27b4038063fad5d023c0d2af06d7277a9b7ee02cc2d366e458f195b4"
content-hash = "57654abddded59b21f10fc936a3f75bf5897bc0fd9ebcc81da38a5699d7ca90a"

View File

@@ -14,6 +14,7 @@ peewee = "^3.17.8"
uvloop = "^0.21.0"
jurigged = "^0.6.0"
psycopg2-binary = "^2.9.10"
apscheduler = "^3.11.0"
[build-system]

7
run.sh
View File

@@ -1,7 +0,0 @@
#!/bin/sh
set -e
export $(grep -E -v '^#' .env | xargs)
docker stack deploy -c docker-compose.yml aboba

View File

@@ -1,11 +0,0 @@
[supervisord]
nodaemon=false
[program:mybot]
command=python main.py
directory=/app
user=root
autostart=true
autorestart=true
stderr_logfile=/app/bot_err.log
stdout_logfile=/app/bot_out.log