chore: сделал

This commit is contained in:
protokey committed 2026-09-26 17:40:47 +04:00
1 parent 602199d510
commit 45e7a1d22f
25 files changed
+1164 -113

No files matched your search

+140 -4
View File
@@ -4,7 +4,7 @@ import re
import serverus
from Telegram.interfaces import Interface, command, register, state, text
from .. import services, tasks
from .. import fields, services, tasks
from ..models import Credentials, Point, Server
from . import texts
@@ -18,6 +18,11 @@ SELECT = 'select'
POINT = 'point'
POINT_RELEASE = 'point.release'
POINT_REINSTALL = 'point.reinstall'
POINT_DOMAIN_EDIT = 'point.domain'
POINT_PROXY_EDIT = 'point.proxy'
POINT_KEYS = 'point.keys'
POINT_KEY_ADD = 'point.keys.add'
POINT_KEY_REVOKE = 'point.keys.revoke'
DRAFT_CREDENTIALS = {DRAFT_SERVER: 'server', DRAFT_MASK: 'mask'}
DOMAIN_RE = re.compile(r'^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$')
@@ -149,6 +154,8 @@ class MyPointInterface(Interface):
@state(DRAFT_PROXY)
def set_proxy(self, ctx):
# в прокси может быть пароль
ctx.delete_message()
proxy = ctx.text.strip()
if not is_proxy_url(proxy):
return ctx.reply(texts.PROXY_INVALID, parse_mode=HTML)
@@ -274,15 +281,144 @@ class MyPointInterface(Interface):
@text(texts.NO, state=POINT_RELEASE)
@text(texts.CANCEL, state=POINT_RELEASE)
@text(texts.CANCEL, state=POINT_REINSTALL)
@text(texts.CANCEL, state=POINT_DOMAIN_EDIT)
@text(texts.CANCEL, state=POINT_PROXY_EDIT)
def back_to_point(self, ctx):
point = self.current_point(ctx)
if point is None:
return self.main_menu(ctx)
self.show_point(ctx, point)
# домен и прокси готовой точки: применяются в фоне, панель тем временем работает с прежними настройками
@text(texts.POINT_DOMAIN, state=POINT)
def ask_point_domain(self, ctx):
point = self.ready_point(ctx)
if point:
keyboard = texts.CANCEL_KEYBOARD if point.mask else [[texts.CANCEL, texts.DELETE]]
self.ask(ctx, POINT_DOMAIN_EDIT, texts.domain_prompt(point), keyboard)
@state(POINT_DOMAIN_EDIT)
def change_domain(self, ctx):
domain = ctx.text.strip().lower().rstrip('.')
if not is_domain(domain):
return ctx.reply(texts.DOMAIN_INVALID)
point = self.ready_point(ctx)
if point:
self.start_change(ctx, point, tasks.set_domain, domain)
@text(texts.DELETE, state=POINT_DOMAIN_EDIT)
def delete_point_domain(self, ctx):
point = self.ready_point(ctx)
if point is None:
return
if point.mask:
return ctx.reply(texts.DOMAIN_REQUIRED_BY_MASK)
if not point.domain:
return self.show_point(ctx, point)
self.start_change(ctx, point, tasks.set_domain, '')
@text(texts.POINT_PROXY, state=POINT)
def ask_point_proxy(self, ctx):
point = self.ready_point(ctx)
if point:
self.ask(ctx, POINT_PROXY_EDIT, texts.proxy_prompt(point), [[texts.CF_WARP, texts.CANCEL, texts.DELETE]])
@state(POINT_PROXY_EDIT)
def change_proxy(self, ctx):
# в прокси может быть пароль
ctx.delete_message()
proxy = ctx.text.strip()
if not is_proxy_url(proxy):
return ctx.reply(texts.PROXY_INVALID, parse_mode=HTML)
self.start_proxy_change(ctx, proxy)
@text(texts.CF_WARP, state=POINT_PROXY_EDIT)
def change_proxy_to_warp(self, ctx):
self.start_proxy_change(ctx, Point.CF_WARP)
@text(texts.DELETE, state=POINT_PROXY_EDIT)
def delete_point_proxy(self, ctx):
self.start_proxy_change(ctx, '')
def start_proxy_change(self, ctx, proxy: str):
point = self.ready_point(ctx)
if point:
self.start_change(ctx, point, tasks.set_outbound_proxy, fields.encrypt(proxy) if proxy else '')
def start_change(self, ctx, point: Point, actor, value: str):
point.set_status(Point.Status.UPDATING)
actor.send(point.pk, ctx.chat.pk, value)
ctx.reset()
ctx.reply(texts.CHANGE_STARTED, keyboard=texts.MAIN_KEYBOARD_WITH_POINTS)
# ключи
def ready_point(self, ctx) -> Point | None:
"""Текущая точка, если на ее панели можно выпускать ключи. Иначе отвечает пользователю и возвращает None."""
point = self.idle_point(ctx)
if point is not None and (point.status != Point.Status.READY or not point.panel_inbound_id):
ctx.reply(texts.POINT_NOT_READY)
return None
return point
@text(texts.CLIENTS, state=POINT)
def not_implemented(self, ctx):
# TODO: настройки домена, прокси и клиенты через API панели 3x-ui
ctx.reply(texts.NOT_IMPLEMENTED)
@text(texts.CLIENTS, state=POINT_KEYS)
@text(texts.CANCEL, state=POINT_KEY_ADD)
@text(texts.CANCEL, state=POINT_KEY_REVOKE)
def show_keys(self, ctx):
point = self.ready_point(ctx)
if point is None:
return
ctx.state = POINT_KEYS
keys = [(key, services.key_traffic(key)) for key in point.keys.all()]
ctx.reply(texts.keys_list(point, keys), keyboard=texts.KEYS_KEYBOARD, parse_mode=HTML)
@state(POINT_KEYS)
def show_key(self, ctx):
point = self.ready_point(ctx)
if point is None:
return
key = point.keys.find_by_name(ctx.text.strip().lower())
if key is None:
return ctx.reply(texts.KEY_NOT_FOUND)
ctx.reply(texts.key_card(key, services.key_traffic(key)), parse_mode=HTML)
@text(texts.ADD_KEY, state=POINT_KEYS)
def ask_key_name(self, ctx):
if self.ready_point(ctx):
self.ask(ctx, POINT_KEY_ADD, texts.KEY_NAME_PROMPT)
@state(POINT_KEY_ADD)
def add_key(self, ctx):
point = self.ready_point(ctx)
if point is None:
return
try:
key = services.issue_key(point, ctx.text.strip().lower())
except ValueError as error:
return ctx.reply(str(error))
except serverus.XuiApiError as error:
return ctx.reply(texts.PANEL_ERROR.format(error=error))
ctx.state = POINT_KEYS
ctx.reply(texts.key_card(key, None), keyboard=texts.KEYS_KEYBOARD, parse_mode=HTML)
@text(texts.REVOKE_KEY, state=POINT_KEYS)
def ask_revoke_key(self, ctx):
if self.ready_point(ctx):
self.ask(ctx, POINT_KEY_REVOKE, texts.KEY_REVOKE_PROMPT)
@state(POINT_KEY_REVOKE)
def revoke_key(self, ctx):
point = self.ready_point(ctx)
if point is None:
return
key = point.keys.find_by_name(ctx.text.strip().lower())
if key is None:
return ctx.reply(texts.KEY_NOT_FOUND)
try:
services.revoke_key(key)
except serverus.XuiApiError as error:
return ctx.reply(texts.PANEL_ERROR.format(error=error))
ctx.reply(texts.KEY_REVOKED.format(name=key.name))
self.show_keys(ctx)