vpnflow 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. vpnflow/__init__.py +8 -0
  2. vpnflow/__main__.py +61 -0
  3. vpnflow/bot/__init__.py +1 -0
  4. vpnflow/bot/base.py +121 -0
  5. vpnflow/bot/callbacks.py +36 -0
  6. vpnflow/bot/commands.py +15 -0
  7. vpnflow/bot/filters.py +28 -0
  8. vpnflow/bot/keyboards/__init__.py +2 -0
  9. vpnflow/bot/keyboards/inline.py +162 -0
  10. vpnflow/bot/middleware.py +78 -0
  11. vpnflow/bot/routers/__init__.py +4 -0
  12. vpnflow/bot/routers/admin.py +218 -0
  13. vpnflow/bot/routers/base.py +481 -0
  14. vpnflow/bot/routers/errors.py +39 -0
  15. vpnflow/bot/states.py +36 -0
  16. vpnflow/cli.py +56 -0
  17. vpnflow/db/__init__.py +2 -0
  18. vpnflow/db/base.py +78 -0
  19. vpnflow/db/events.py +44 -0
  20. vpnflow/db/models.py +199 -0
  21. vpnflow/db/repositories.py +318 -0
  22. vpnflow/db/tools.py +105 -0
  23. vpnflow/enums.py +5 -0
  24. vpnflow/flows/__init__.py +1 -0
  25. vpnflow/flows/_dagster.py +37 -0
  26. vpnflow/flows/_schedule.py +39 -0
  27. vpnflow/flows/base.py +55 -0
  28. vpnflow/log/__init__.py +1 -0
  29. vpnflow/log/telegram/__init__.py +1 -0
  30. vpnflow/log/telegram/formatters.py +69 -0
  31. vpnflow/log/telegram/handlers.py +142 -0
  32. vpnflow/misc.py +29 -0
  33. vpnflow/services/__init__.py +2 -0
  34. vpnflow/services/_marzban.py +78 -0
  35. vpnflow/services/_redis.py +7 -0
  36. vpnflow/services/_telegram.py +122 -0
  37. vpnflow/services/business.py +548 -0
  38. vpnflow/services/cache.py +105 -0
  39. vpnflow/settings.py +172 -0
  40. vpnflow/web/__init__.py +1 -0
  41. vpnflow/web/app.py +61 -0
  42. vpnflow/web/schemas.py +206 -0
  43. vpnflow/web/views.py +67 -0
  44. vpnflow-1.0.0.dist-info/LICENSE +661 -0
  45. vpnflow-1.0.0.dist-info/METADATA +54 -0
  46. vpnflow-1.0.0.dist-info/RECORD +48 -0
  47. vpnflow-1.0.0.dist-info/WHEEL +4 -0
  48. vpnflow-1.0.0.dist-info/entry_points.txt +3 -0
vpnflow/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ # -*- coding: utf-8 -*-
2
+ __author__ = "Michael R. Kisel"
3
+ __copyright__ = "Michael R. Kisel"
4
+ __license__ = "AGPL"
5
+ __version__ = "1.0.0"
6
+ __maintainer__ = "Michael R. Kisel"
7
+ __email__ = "aioboy@yandex.com"
8
+ __status__ = "Stable"
vpnflow/__main__.py ADDED
@@ -0,0 +1,61 @@
1
+ # -*- coding: utf-8 -*-
2
+ from asyncio import get_event_loop
3
+ from logging import getLogger
4
+
5
+ import uvicorn
6
+ from aiogram.exceptions import TelegramRetryAfter
7
+
8
+ from vpnflow.bot.base import create_dispatcher
9
+ from vpnflow.cli import create_parser, show_art
10
+ from vpnflow.db.tools import init_db
11
+ from vpnflow.flows._schedule import run_scheduled_tasks
12
+ from vpnflow.misc import load_log_conf
13
+ from vpnflow.settings import settings
14
+
15
+ logger = getLogger(__name__)
16
+
17
+
18
+ def main():
19
+ """⭐"""
20
+ show_art()
21
+ parser, loop = create_parser(), get_event_loop()
22
+
23
+ args = parser.parse_args()
24
+
25
+ if args.log_conf_file:
26
+ load_log_conf(args.log_conf_file)
27
+
28
+ logger.debug(f"Run with args: {args}, settings: {settings}")
29
+
30
+ if args.run_scheduled_tasks:
31
+ run_scheduled_tasks()
32
+
33
+ if args.command == "db":
34
+ loop.run_until_complete(init_db(args))
35
+
36
+ if settings.telegram.webhook_use:
37
+ uvicorn.run(
38
+ "vpnflow.web.app:app",
39
+ host=settings.webserver.host, port=settings.webserver.port,
40
+ workers=settings.webserver.workers, reload=settings.webserver.reload
41
+ )
42
+ else:
43
+ bot_dispatcher = create_dispatcher()
44
+ try:
45
+ loop.run_until_complete(
46
+ bot_dispatcher.start_polling(
47
+ bot_dispatcher.bot,
48
+ allowed_updates=bot_dispatcher.resolve_used_update_types(),
49
+ polling_timeout=60, skip_updates=True
50
+ )
51
+ )
52
+ except KeyboardInterrupt:
53
+ loop.run_until_complete(bot_dispatcher.stop_polling())
54
+ except TelegramRetryAfter as exc:
55
+ logger.error(exc)
56
+ except Exception as exc:
57
+ logger.exception(exc)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
@@ -0,0 +1 @@
1
+ # -*- coding: utf-8 -*-
vpnflow/bot/base.py ADDED
@@ -0,0 +1,121 @@
1
+ # -*- coding: utf-8 -*-
2
+ from logging import getLogger
3
+ from typing import Optional, Tuple
4
+
5
+ from aiogram import Bot, Dispatcher
6
+ from aiogram.client.default import DefaultBotProperties
7
+ from aiogram.client.session.aiohttp import AiohttpSession
8
+ from aiogram.enums import ParseMode
9
+ from aiogram.fsm.storage.base import BaseStorage
10
+ from aiogram.fsm.storage.memory import MemoryStorage
11
+ from aiogram.fsm.storage.redis import RedisStorage
12
+ from aiogram.types.bot_command_scope_chat import BotCommandScopeChat
13
+ from aiogram.utils.callback_answer import CallbackAnswerMiddleware
14
+
15
+ from vpnflow.bot import commands, middleware, routers
16
+ from vpnflow.services import _redis
17
+ from vpnflow.services._telegram import broadcast
18
+ from vpnflow.services.cache import CacheRepository
19
+ from vpnflow.settings import Settings, TelegramSettings, settings
20
+
21
+ logger = getLogger(__name__)
22
+ telegram_settings = settings.telegram
23
+
24
+
25
+ async def on_startup_bot(
26
+ bot: Bot,
27
+ bot_dispatcher: Dispatcher,
28
+ settings_telegram: TelegramSettings = telegram_settings
29
+ ):
30
+ """⭐"""
31
+ logger.info("Startup bot")
32
+ bot_info = await bot.get_me()
33
+ logger.info(f"Name: {bot_info.full_name}. Username: @{bot_info.username}. ID: {bot_info.id}")
34
+ assert bot_info.username == settings.telegram.bot_name
35
+ await broadcast(bot, settings_telegram.messages["bot-on"], *settings_telegram.admins_id)
36
+ await bot.delete_my_commands()
37
+ await bot.set_my_commands(commands.user)
38
+ for admin_id in settings_telegram.admins_id:
39
+ await bot.set_my_commands(commands.admin, scope=BotCommandScopeChat(chat_id=admin_id))
40
+ if settings_telegram.webhook_use:
41
+ webhook_info = await bot.get_webhook_info()
42
+ logger.info(f"Webhook info: {webhook_info}")
43
+ if webhook_info.url != settings_telegram.webhook_url:
44
+ await bot.delete_webhook(drop_pending_updates=True)
45
+ await bot.set_webhook(
46
+ url=settings_telegram.webhook_url, drop_pending_updates=True,
47
+ allowed_updates=bot_dispatcher.resolve_used_update_types(),
48
+ max_connections=settings_telegram.webhook_max_connections,
49
+ secret_token=settings_telegram.webhook_secret_token.get_secret_value()
50
+ )
51
+ is_health = await CacheRepository.check_health()
52
+ assert is_health
53
+
54
+
55
+ async def on_shutdown_bot(
56
+ bot: Bot,
57
+ bot_dispatcher: Dispatcher,
58
+ settings_telegram: TelegramSettings = telegram_settings
59
+ ):
60
+ """⭐"""
61
+ logger.info("Shutdown bot")
62
+ if isinstance(bot_dispatcher.storage, RedisStorage):
63
+ await bot_dispatcher.storage.close()
64
+ await bot_dispatcher.fsm.storage.close()
65
+ await broadcast(bot, settings_telegram.messages["bot-off"], *settings_telegram.admins_id)
66
+ # if settings_telegram.webhook_use:
67
+ # await bot.delete_webhook(drop_pending_updates=True)
68
+ await bot.session.close()
69
+ await bot.close()
70
+ await CacheRepository.close()
71
+
72
+
73
+ async def on_startup_bot_dispatcher(dispatcher) -> None:
74
+ """⭐"""
75
+ logger.info("Startup bot dispatcher")
76
+ await on_startup_bot(dispatcher.bot, dispatcher)
77
+
78
+
79
+ async def on_shutdown_bot_dispatcher(dispatcher) -> None:
80
+ """⭐"""
81
+ logger.info("Shutdown bot dispatcher")
82
+ await on_shutdown_bot(dispatcher.bot, dispatcher)
83
+
84
+
85
+ def create_bot(
86
+ settings_telegram: TelegramSettings = telegram_settings,
87
+ session: Optional[AiohttpSession] = None
88
+ ) -> Bot:
89
+ """⭐"""
90
+ if session is None:
91
+ session: AiohttpSession = AiohttpSession()
92
+ return Bot(
93
+ token=settings_telegram.bot_token.get_secret_value(),
94
+ default=DefaultBotProperties(parse_mode=ParseMode.HTML),
95
+ session=session
96
+ )
97
+
98
+
99
+ def create_dispatcher(
100
+ bot: Bot = create_bot(),
101
+ middlewares: Tuple = middleware.ALL,
102
+ routers: Tuple = routers.ALL,
103
+ settings: Settings = settings,
104
+ on_startup = on_startup_bot_dispatcher,
105
+ on_shutdown = on_shutdown_bot_dispatcher
106
+ ) -> Dispatcher:
107
+ """⭐"""
108
+ redis_url = settings.telegram.redis_url.get_secret_value()
109
+ if redis_url:
110
+ storage: BaseStorage = RedisStorage(redis=_redis.create_client(redis_url))
111
+ else:
112
+ storage: BaseStorage = MemoryStorage()
113
+ dispatcher = Dispatcher(name="bot_dispatcher", storage=storage)
114
+ dispatcher.bot = bot
115
+ for _middleware in middlewares:
116
+ dispatcher.update.middleware.register(_middleware)
117
+ dispatcher.callback_query.middleware(CallbackAnswerMiddleware())
118
+ dispatcher.include_routers(*routers)
119
+ dispatcher.startup.register(on_startup)
120
+ dispatcher.shutdown.register(on_shutdown)
121
+ return dispatcher
@@ -0,0 +1,36 @@
1
+ # -*- coding: utf-8 -*-
2
+ from typing import Any, Optional, Union
3
+
4
+ from aiogram.filters.callback_data import CallbackData
5
+
6
+
7
+ class PayPlanCallback(CallbackData, prefix="payplan"):
8
+ """⭐"""
9
+ action: str
10
+ value: Union[int, str]
11
+
12
+
13
+ class YesNoCallback(CallbackData, prefix="yesno"):
14
+ """⭐"""
15
+ value: bool
16
+
17
+
18
+ class AdminCallback(CallbackData, prefix="admin"):
19
+ """⭐"""
20
+ action: str
21
+
22
+
23
+ class SupportCallback(CallbackData, prefix="support"):
24
+ """⭐"""
25
+ action: str
26
+
27
+
28
+ class UserCallback(CallbackData, prefix="user"):
29
+ """⭐"""
30
+ action: str
31
+ value: Optional[Any] = None
32
+
33
+
34
+ class NotifyCallback(CallbackData, prefix="notify"):
35
+ """⭐"""
36
+ value: str
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+ from aiogram.types import BotCommand
3
+
4
+ from vpnflow.enums import BotCommands, BotCommandsAdmin
5
+ from vpnflow.settings import settings
6
+
7
+
8
+ def build_menu_from_enum(enum, commands_text=settings.telegram.messages["commands"]):
9
+ """⭐"""
10
+ for k, _ in enum.__members__.items():
11
+ yield BotCommand(command=f"/{k}", description=commands_text[k])
12
+
13
+
14
+ user = list(build_menu_from_enum(BotCommands))
15
+ admin = user + list(build_menu_from_enum(BotCommandsAdmin))
vpnflow/bot/filters.py ADDED
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8 -*-
2
+ from aiogram.filters import BaseFilter
3
+ from aiogram.types import Message
4
+
5
+ from vpnflow.settings import settings
6
+
7
+
8
+ class AdminFilter(BaseFilter):
9
+ """⭐"""
10
+
11
+ ADMINS = settings.telegram.admins_id
12
+
13
+ async def __call__(self, event: Message) -> bool:
14
+ user = event.from_user
15
+ if user:
16
+ return user.id in self.ADMINS
17
+ return False
18
+
19
+
20
+ class BlacklistFilter(BaseFilter):
21
+ """⭐"""
22
+
23
+ BLACKLIST = set(settings.telegram.blacklist)
24
+
25
+ async def __call__(self, event: Message) -> bool:
26
+ user = event.from_user
27
+ if user:
28
+ return False if user.id in self.BLACKLIST else True
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ from . import inline
@@ -0,0 +1,162 @@
1
+ # -*- coding: utf-8 -*-
2
+ from functools import lru_cache
3
+
4
+ from aiogram.types import WebAppInfo
5
+ from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardMarkup
6
+
7
+ from vpnflow.bot import callbacks as cb
8
+ from vpnflow.settings import settings
9
+
10
+ buttons_text = settings.telegram.messages["buttons"]
11
+
12
+
13
+ def create_inline_keyboard(*buttons_data, rows_num=None, cols_num=None, attach=None, as_markup=True):
14
+ """⭐"""
15
+ kb = InlineKeyboardBuilder()
16
+ for button_text, button_callback in buttons_data:
17
+ kb.button(text=button_text, callback_data=button_callback)
18
+ if attach:
19
+ kb.attach(attach)
20
+ if rows_num:
21
+ if cols_num:
22
+ kb.adjust(rows_num, cols_num)
23
+ else:
24
+ kb.adjust(rows_num)
25
+ if as_markup:
26
+ return kb.as_markup()
27
+ return kb
28
+
29
+
30
+ @lru_cache(maxsize=32)
31
+ def build_referral(url: str) -> InlineKeyboardMarkup:
32
+ """⭐"""
33
+ kb = InlineKeyboardBuilder()
34
+ kb.button(text=buttons_text["referral-share"], url=f"https://telegram.me/share/url?url={url}")
35
+ kb.button(text=buttons_text["start"], callback_data=cb.UserCallback(action="start"))
36
+ kb.adjust(1)
37
+ return kb.as_markup()
38
+
39
+
40
+ @lru_cache(maxsize=32)
41
+ def build_setup(url: str) -> InlineKeyboardMarkup:
42
+ """⭐"""
43
+ kb = InlineKeyboardBuilder()
44
+ kb.button(text=buttons_text["setup-open"], web_app=WebAppInfo(url=url))
45
+ return kb.as_markup()
46
+
47
+
48
+ async def build_pay_plans(pay_plans_agenerator) -> InlineKeyboardMarkup:
49
+ """⭐"""
50
+ kb_data = []
51
+ async for pay_plan in pay_plans_agenerator:
52
+ kb_data.append(
53
+ (
54
+ pay_plan.button_text,
55
+ cb.PayPlanCallback(action="choice-pay-plan", value=pay_plan.id))
56
+ )
57
+ kb_data.append((buttons_text["back"], cb.UserCallback(action="start")))
58
+ return create_inline_keyboard(*kb_data, rows_num=1)
59
+
60
+
61
+ def build_pay_plan_prices(pay_plan) -> InlineKeyboardMarkup:
62
+ """⭐"""
63
+ kb_data = []
64
+ for pay_plan_price in pay_plan.prices:
65
+ kb_data.append(
66
+ (
67
+ pay_plan_price.currency.symbol,
68
+ cb.PayPlanCallback(action="choice-pay-method", value=pay_plan_price.payment_method))
69
+ )
70
+ return create_inline_keyboard(*kb_data, rows_num=2)
71
+
72
+
73
+ @lru_cache(maxsize=32)
74
+ def build_setup_steps(download_url: str, add_url: str) -> InlineKeyboardMarkup:
75
+ """⭐"""
76
+ kb = InlineKeyboardBuilder()
77
+ kb.button(text=buttons_text["setup-download"], url=download_url)
78
+ kb.button(text=buttons_text["setup-add"], url=add_url)
79
+ kb.button(text=buttons_text["start"], callback_data=cb.UserCallback(action="start"))
80
+ kb.adjust(1)
81
+ return kb.as_markup()
82
+
83
+
84
+ support = InlineKeyboardBuilder()
85
+ support.button(text=buttons_text["help-faq"], web_app=WebAppInfo(url=settings.business.url_faq))
86
+ support.button(text=buttons_text["help-ask"], url=settings.business.url_support)
87
+ support.button(text=buttons_text["setup-help"], callback_data=cb.UserCallback(action="setup-help"))
88
+ support.button(text=buttons_text["back"], callback_data=cb.UserCallback(action="start"))
89
+ support.adjust(1)
90
+ support = support.as_markup()
91
+
92
+ support_back = InlineKeyboardBuilder()
93
+ support_back.button(text=buttons_text["back"], callback_data=cb.UserCallback(action="help"))
94
+ support_back.adjust(1)
95
+ support_back = support_back.as_markup()
96
+
97
+ yes_no_menu = create_inline_keyboard(
98
+ *(
99
+ (buttons_text["choice-yes"], cb.YesNoCallback(value=True)),
100
+ (buttons_text["choice-no"], cb.YesNoCallback(value=False))
101
+ ), rows_num=2
102
+ )
103
+
104
+ user_promo = create_inline_keyboard(
105
+ *(
106
+ (buttons_text["promo-again"], cb.UserCallback(action="promo")),
107
+ (buttons_text["start"], cb.UserCallback(action="start"))
108
+ ), rows_num=1
109
+ )
110
+
111
+ user_welcome = InlineKeyboardBuilder()
112
+ user_welcome.button(text=buttons_text["start-accept"], callback_data=cb.UserCallback(action="sign_up"))
113
+ user_welcome.adjust(1)
114
+ user_welcome = user_welcome.as_markup()
115
+
116
+ user_start = InlineKeyboardBuilder()
117
+ user_start.button(text=buttons_text["start"], callback_data=cb.UserCallback(action="start"))
118
+ user_start = user_start.as_markup()
119
+
120
+ user_start_new = InlineKeyboardBuilder()
121
+ user_start_new.button(text=buttons_text["start-setup"], callback_data=cb.UserCallback(action="setup"))
122
+ user_start_new = user_start_new.as_markup()
123
+
124
+ user_setup_platform = create_inline_keyboard(
125
+ *(
126
+ (
127
+ platform, cb.UserCallback(action="setup-platform", value=platform)
128
+ ) for platform in settings.business.supported_platforms
129
+ ), rows_num=4
130
+ )
131
+
132
+ user = create_inline_keyboard(
133
+ *(
134
+ (
135
+ buttons_text[k], cb.UserCallback(action=k)
136
+ ) for k in ("pay", "setup", "promo", "invite", "help")
137
+ ), rows_num=1
138
+ )
139
+
140
+
141
+ admin = create_inline_keyboard(
142
+ *(
143
+ (
144
+ buttons_text[f"admin-{k}"], cb.AdminCallback(action=k)
145
+ ) for k in ("stats", "notify", "coupons", "services")
146
+ ), rows_num=1
147
+ )
148
+
149
+ admin_notify = create_inline_keyboard(
150
+ *(
151
+ (buttons_text["admin-notify-all"], cb.NotifyCallback(value="all")),
152
+ (buttons_text["admin-notify-active"], cb.NotifyCallback(value="active")),
153
+ (buttons_text["admin-notify-expired"], cb.NotifyCallback(value="expired")),
154
+ (buttons_text["admin-notify-pay-expired"], cb.NotifyCallback(value="pay-expired")),
155
+ (buttons_text["admin-notify-pay-no-expired"], cb.NotifyCallback(value="pay-no-expired")),
156
+ (buttons_text["back"], cb.AdminCallback(action="panel"))
157
+ ), rows_num=1
158
+ )
159
+
160
+ admin_start = InlineKeyboardBuilder()
161
+ admin_start.button(text=buttons_text["panel"], callback_data=cb.AdminCallback(action="panel"))
162
+ admin_start = admin_start.as_markup()
@@ -0,0 +1,78 @@
1
+ # -*- coding: utf-8 -*-
2
+ from logging import getLogger
3
+ from typing import Any, Awaitable, Callable, Dict
4
+
5
+ from aiogram import BaseMiddleware
6
+ from aiogram.types import CallbackQuery, Message, TelegramObject
7
+
8
+ from vpnflow.bot import keyboards as kbs
9
+ from vpnflow.bot.callbacks import UserCallback
10
+ from vpnflow.services import business
11
+ from vpnflow.services._telegram import edit_callback
12
+ from vpnflow.settings import settings
13
+
14
+ logger = getLogger(__name__)
15
+ messages = settings.telegram.messages
16
+
17
+
18
+ class CheckAcceptMiddleware(BaseMiddleware):
19
+ """⭐"""
20
+
21
+ def __init__(
22
+ self,
23
+ callbacks = ("start", "pay", "setup", "promo", "invite", "help", "setup-platform", "setup-help")
24
+ ) -> None:
25
+ self.callbacks = callbacks
26
+
27
+ async def __call__(
28
+ self,
29
+ handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
30
+ event: TelegramObject,
31
+ data: Dict[str, Any],
32
+ ) -> Any:
33
+ if isinstance(event, Message) and "/start" not in str(event.text):
34
+ user = await business.get_user(telegram_id=event.from_user.id)
35
+ if user and user.marzban_username is None:
36
+ await event.answer(messages["start-welcome"], reply_markup=kbs.inline.user_welcome)
37
+ return
38
+ else:
39
+ data["user"] = user
40
+ if user is None:
41
+ logger.warning(f"User is None, {event.from_user.id}")
42
+ return
43
+ if isinstance(event, CallbackQuery):
44
+ callback_data = data.get("callback_data")
45
+ if (
46
+ callback_data and
47
+ isinstance(callback_data, UserCallback) and
48
+ callback_data.action in self.callbacks
49
+ ):
50
+ user = await business.get_user(telegram_id=event.from_user.id)
51
+ if user and user.marzban_username is None:
52
+ await edit_callback(event, messages["start-welcome"], reply_markup=kbs.inline.user_welcome)
53
+ return
54
+ else:
55
+ data["user"] = user
56
+ if user is None:
57
+ logger.warning(f"User is None, {event.from_user.id}")
58
+ return
59
+ result = await handler(event, data)
60
+ return result
61
+
62
+
63
+ class FilterNoUserMiddleware(BaseMiddleware):
64
+ """⭐"""
65
+
66
+ async def __call__(
67
+ self,
68
+ handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
69
+ event: TelegramObject,
70
+ data: Dict[str, Any],
71
+ ) -> Any:
72
+ if isinstance(event, (CallbackQuery, Message)) and event.from_user is None:
73
+ return
74
+ result = await handler(event, data)
75
+ return result
76
+
77
+
78
+ ALL = (FilterNoUserMiddleware(), )
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+ from vpnflow.bot.routers import admin, base, errors
3
+
4
+ ALL = (base.router, errors.router, admin.router)