├── requirements.txt ├── bot ├── handlers │ ├── __init__.py │ ├── start.py │ ├── add_client.py │ └── get_client.py ├── states.py ├── loader.py ├── keyboards.py ├── app.py ├── data.py └── shell_interface.py ├── .assets └── preview.gif ├── .github └── FUNDING.yml ├── Makefile ├── README.md └── wg-manager.sh /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==2.19 2 | environs==9.5.0 3 | loguru==0.6.0 -------------------------------------------------------------------------------- /bot/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import start, get_client, add_client 2 | -------------------------------------------------------------------------------- /.assets/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheK4n/wireguard-manager/HEAD/.assets/preview.gif -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | custom: ['https://qiwi.com/n/THREA793', 'https://www.blockchain.com/btc/address/bc1qgh2fyzqgyxgpstsmqwxyev2luzx7hwc4ezq03u', 'https://www.blockchain.com/eth/address/0x01931cf08BbbA74629c232DbEDB390798cDD121f'] 3 | -------------------------------------------------------------------------------- /bot/states.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.filters.state import StatesGroup, State 2 | 3 | 4 | class GetClient(StatesGroup): 5 | name = State() 6 | choice = State() 7 | del_confirm = State() 8 | 9 | 10 | class AddClient(StatesGroup): 11 | name = State() 12 | -------------------------------------------------------------------------------- /bot/loader.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, Dispatcher, types 2 | from aiogram.contrib.fsm_storage.memory import MemoryStorage 3 | 4 | from data import BOT_TOKEN 5 | from loguru import logger 6 | 7 | 8 | logger.add("wg_manager.log", format="{time} {level} {message}", level="DEBUG", rotation="20 MB", compression="gz") 9 | 10 | bot = Bot(token=BOT_TOKEN, parse_mode=types.ParseMode.HTML) 11 | storage = MemoryStorage() 12 | dp = Dispatcher(bot, storage=storage) 13 | -------------------------------------------------------------------------------- /bot/keyboards.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup 2 | 3 | from data import ButtonText 4 | 5 | menu = InlineKeyboardMarkup( 6 | inline_keyboard=[ 7 | [ 8 | InlineKeyboardButton(text=ButtonText.ADD_CLIENT, callback_data='add_client'), 9 | ], 10 | [ 11 | InlineKeyboardButton(text=ButtonText.CLIENTS, callback_data="clients:0"), 12 | ], 13 | 14 | ] 15 | ) 16 | cancel = InlineKeyboardButton(text=ButtonText.BACK_MENU, callback_data="cancel") 17 | cancel_inline = InlineKeyboardMarkup(row_width=2) 18 | cancel_inline.insert(cancel) 19 | 20 | 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: init 2 | 3 | init: 4 | mkdir -p $(HOME)/.local/bin 5 | chmod +x $(PWD)/wg-manager.sh 6 | ln -s $(PWD)/wg-manager.sh $(HOME)/.local/bin/wg-manager 7 | 8 | tg: 9 | python3 -m virtualenv venv 10 | $(PWD)/venv/bin/pip install -r requirements.txt 11 | echo "WG_MANAGER_PATH=$(PWD)/wg-manager.sh" >> .env 12 | echo "\ 13 | [Unit]\n\ 14 | Description=wireguard\n\ 15 | After=network.target\n\ 16 | \n\ 17 | [Service]\n\ 18 | Type=simple\n\ 19 | WorkingDirectory= $(PWD)\n\ 20 | ExecStart=$(PWD)/venv/bin/python3 $(PWD)/bot/app.py\n\ 21 | Restart=on-failure\n\ 22 | [Install]\n\ 23 | WantedBy=default.target" > /etc/systemd/system/wg_manager.service 24 | systemctl enable wg_manager 25 | systemctl start wg_manager 26 | -------------------------------------------------------------------------------- /bot/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiogram import executor, types 4 | from loader import dp, logger 5 | import handlers 6 | 7 | 8 | async def set_default_commands(dispatcher): 9 | await dispatcher.bot.set_my_commands( 10 | [ 11 | types.BotCommand("start", "Start bot"), 12 | types.BotCommand("menu", "Menu"), 13 | types.BotCommand("help", "Help"), 14 | ] 15 | ) 16 | 17 | 18 | async def on_startup(dispatcher): 19 | await asyncio.sleep(1) 20 | await set_default_commands(dispatcher) 21 | logger.info("Bot started") 22 | 23 | 24 | async def on_shutdown(dispatcher): 25 | logger.info("Bot stopped") 26 | await dispatcher.storage.close() 27 | await dispatcher.storage.wait_closed() 28 | 29 | 30 | if __name__ == '__main__': 31 | executor.start_polling(dp, on_startup=on_startup, on_shutdown=on_shutdown) 32 | -------------------------------------------------------------------------------- /bot/handlers/start.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.dispatcher.filters.builtin import CommandStart, CommandHelp, Command 3 | 4 | from loader import dp, logger 5 | from data import ADMINS, Text 6 | from keyboards import menu 7 | 8 | 9 | async def base_handler(message: types.Message, answer_text: str, **kwargs): 10 | state = dp.current_state(user=message.from_user.id) 11 | await state.reset_state() 12 | logger.info(f"{message.text} from user {message.from_user.username}:{message.from_user.id}") 13 | await message.answer(answer_text, **kwargs) 14 | 15 | 16 | @dp.message_handler(CommandStart(), state='*', user_id=ADMINS) 17 | async def bot_start(message: types.Message): 18 | await base_handler(message, Text.START.format(name=message.from_user.full_name), reply_markup=menu) 19 | 20 | 21 | @dp.message_handler(CommandHelp(), state='*', user_id=ADMINS) 22 | async def bot_help(message: types.Message): 23 | await base_handler(message, Text.HELP) 24 | 25 | 26 | @dp.message_handler(Command("menu"), state='*', user_id=ADMINS) 27 | async def bot_menu(message: types.Message): 28 | await base_handler(message, Text.MENU, reply_markup=menu) 29 | -------------------------------------------------------------------------------- /bot/data.py: -------------------------------------------------------------------------------- 1 | from environs import Env 2 | 3 | env = Env() 4 | env.read_env() 5 | 6 | BOT_TOKEN = env.str("TOKEN") 7 | ADMINS = env.list("ADMINS") 8 | WG_MANAGER_PATH = env.str("WG_MANAGER_PATH") 9 | 10 | 11 | class ReturnCodes: 12 | SystemFail = 1 13 | PeerNotExists = 10 14 | ValidationError = 11 15 | PeerAlreadyExists = 12 16 | SubnetError = 24 17 | 18 | 19 | class Text: 20 | MENU = "WireGuard Manager bot menu" 21 | HELP = "WireGuard Manager bot\n\nGithub: https://github.com/thek4n/wireguard-manager \nManual: https://telegra.ph/Instrukciya-po-ustanovke-klienta-Wireguard-VPN-04-19" 22 | START = "Hello, Access allowed to {name}" 23 | CLIENTS = "Clients" 24 | CLIENT = "Client \"{client_name}\"" 25 | CLIENT_ADDED = "Client \"{client_name}\" was added, here his QR code" 26 | CLIENT_DELETED = "Client \"{client_name}\" was deleted" 27 | CLIENT_DELETE_CONFIRM = "You really want to delete client \"{client_name}\"?" 28 | ASK_NAME = "Send me a name for a new client" 29 | ERROR_1 = "System fail, check logs" 30 | ERROR_11 = "Wrong name, you can use letters, numbers and \"-\", \"_\"" 31 | ERROR_10 = "Client \"{client_name}\" does not exists" 32 | ERROR_12 = "Client \"{client_name}\" already exists" 33 | ERROR_24 = "24 net supports only 253 peers" 34 | 35 | 36 | class ButtonText: 37 | ADD_CLIENT = "Add client" 38 | BACK_MENU = "<< Back to menu" 39 | GET_QR = "Get QR Code" 40 | GET_FILE = "Get file" 41 | GET_RAW = "Get raw" 42 | DELETE = "Delete" 43 | CONFIRM = "Confirm!" 44 | CLIENTS = "Clients" 45 | -------------------------------------------------------------------------------- /bot/shell_interface.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from io import BytesIO 3 | 4 | from data import WG_MANAGER_PATH 5 | 6 | 7 | def execute_command(command: str, *args) -> subprocess.CompletedProcess: 8 | command_ = ["bash", WG_MANAGER_PATH, command] 9 | for i in args: 10 | command_.append(i) 11 | command_.append("") 12 | return subprocess.run(command_, capture_output=True) 13 | 14 | 15 | def get_clients_from_manager() -> list: 16 | return execute_command("ls").stdout.decode().strip().split("\n") 17 | 18 | 19 | def get_config_qrcode(client_name: str) -> bytes: 20 | return execute_command("get_tg", client_name).stdout 21 | 22 | 23 | def raw_to_html(raw: str) -> str: 24 | res = '' 25 | for i in raw.split("\n"): 26 | pre = i.split(" = ") 27 | try: 28 | if pre[0] in ("PrivateKey", "PresharedKey"): 29 | res += f"{pre[0]} = {pre[1]}\n" 30 | else: 31 | res += f"{pre[0]} = {pre[1]}\n" 32 | except IndexError: 33 | res += i + "\n" 34 | return res.strip() 35 | 36 | 37 | def get_config_raw(client_name: str) -> bytes: 38 | return execute_command("get_file", client_name).stdout 39 | 40 | 41 | def add_client(client_name: str): 42 | return execute_command("add_tg", client_name) 43 | 44 | 45 | def delete_client(client_name: str): 46 | return execute_command("rm", client_name) 47 | 48 | 49 | def put_bytes_to_file(file: bytes) -> BytesIO: 50 | bytes_file = BytesIO(file) 51 | bytes_file.seek(0) 52 | return bytes_file 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Wireguard Manager

5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 | 15 | * [Project description](#chapter-0) 16 | * [Requirements](#chapter-1) 17 | * [Installation](#chapter-2) 18 | * [Usage](#chapter-3) 19 | 20 | 21 | 22 | ## Project description 23 | 24 | Wireguard manager with Telegram integration 25 | 26 |

27 | 28 | 29 | 30 | 31 | 32 | ## Requirements 33 | 34 | * **wg** - Wireguard CLI 35 | * **qrencode** - Generate qrcode 36 | 37 | ## Installation 38 | 39 | ```.env``` example: 40 | ```bash 41 | TOKEN=123456789:ABCDEFG # tg bot token 42 | ADMIN=123456789 # your tg id 43 | ``` 44 | 45 | Clone repository and installing dependencies: 46 | 47 | ```bash 48 | git clone https://github.com/TheK4n/wireguard-manager.git 49 | cd wireguard-manager 50 | make init 51 | wg-manager init 52 | make tg # initialize tg bot 53 | ``` 54 | 55 | 56 | ## Usage 57 | ```bash 58 | wg-manager add "client_name" # Adds client and shows qrcode 59 | ``` 60 | ```bash 61 | wg-manager ls # Clients list 62 | ``` 63 | ```bash 64 | wg-manager get "client_name" # Shows client qrcode 65 | ``` 66 | ```bash 67 | wg-manager rm "client_name" # Removes client 68 | ``` 69 | 70 | 71 | 72 |

73 | -------------------------------------------------------------------------------- /bot/handlers/add_client.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher import FSMContext 2 | from aiogram.types import CallbackQuery, Message, InlineKeyboardMarkup 3 | 4 | from data import Text, ReturnCodes 5 | from keyboards import cancel, menu 6 | from loader import dp, logger 7 | from shell_interface import add_client, put_bytes_to_file 8 | from states import AddClient 9 | 10 | 11 | @dp.callback_query_handler(text="cancel", state=AddClient) 12 | async def cancel_order(call: CallbackQuery, state: FSMContext): 13 | await call.message.edit_text(Text.MENU, reply_markup=menu) 14 | await call.answer() 15 | await state.finish() 16 | 17 | 18 | @dp.callback_query_handler(text_contains="add_client") 19 | async def get_client(call: CallbackQuery, state: FSMContext): 20 | 21 | cancel_menu = InlineKeyboardMarkup() 22 | cancel_menu.insert(cancel) 23 | await call.message.edit_text(Text.ASK_NAME, reply_markup=cancel_menu) 24 | await call.answer() 25 | 26 | await AddClient.name.set() 27 | 28 | 29 | @dp.message_handler(state=AddClient.name) 30 | async def get_client_2(message: Message, state: FSMContext): 31 | client_name = message.text 32 | command_result = add_client(client_name) 33 | 34 | if command_result.returncode: 35 | if command_result.returncode == ReturnCodes.SystemFail: 36 | await message.answer(Text.ERROR_1) 37 | elif command_result.returncode == ReturnCodes.ValidationError: 38 | await message.answer(Text.ERROR_11) 39 | elif command_result.returncode == ReturnCodes.PeerAlreadyExists: 40 | await message.answer(Text.ERROR_12.format(client_name=client_name)) 41 | elif command_result.returncode == ReturnCodes.SubnetError: 42 | await message.answer(Text.ERROR_24) 43 | logger.error(f"adding client {message.text} from user {message.from_user.username}:{message.from_user.id}") 44 | else: 45 | photo = put_bytes_to_file(command_result.stdout) 46 | await message.reply(Text.CLIENT_ADDED.format(client_name=client_name)) 47 | await message.answer_photo(photo=photo) 48 | logger.info(f"added client {message.text} from user {message.from_user.username}:{message.from_user.id}") 49 | 50 | await message.answer(Text.MENU, reply_markup=menu) 51 | await state.finish() 52 | -------------------------------------------------------------------------------- /wg-manager.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | WG_ID=wg0 4 | WG_PREFIX=/etc/wireguard 5 | WG_CONF=$WG_PREFIX/$WG_ID.conf 6 | WG_PARAMS=$WG_PREFIX/params 7 | WG_PEERS=$WG_PREFIX/peers 8 | 9 | WG_SUBNET=10.0.0. 10 | WG_ADDR="$WG_SUBNET"1 11 | WG_PORT=51830 12 | 13 | CLIENT_DNSs=208.67.222.222,208.67.220.220 14 | 15 | 16 | bye() { 17 | echo "$0: Error: $1" >&2 18 | exit $2 19 | } 20 | 21 | is_root() { 22 | if [ "${EUID}" -ne 0 ]; then 23 | bye "You need to run this script as root" 1 24 | fi 25 | } 26 | 27 | is_exists_requirements() { 28 | which "$1" >/dev/null || bye "'$1' not found" 1 29 | } 30 | 31 | check_requirements() { 32 | is_exists_requirements "wg" 33 | is_exists_requirements "qrencode" 34 | } 35 | 36 | check_all() { 37 | is_root 38 | check_requirements 39 | } 40 | 41 | write_params() { 42 | echo "SERVER_PRIVATE_KEY=$SERVER_PRIVATE_KEY" >> "$WG_PARAMS" 43 | echo "SERVER_PUBLIC_KEY=$SERVER_PUBLIC_KEY" >> "$WG_PARAMS" 44 | } 45 | 46 | generate_server_keys() { 47 | SERVER_PRIVATE_KEY=$(wg genkey) 48 | SERVER_PUBLIC_KEY=$(echo "$SERVER_PRIVATE_KEY" | wg pubkey) 49 | } 50 | 51 | create_config_file() { 52 | echo "[Interface]" > $WG_CONF 53 | echo "PrivateKey = $SERVER_PRIVATE_KEY" >> $WG_CONF 54 | echo "Address = $WG_ADDR/24" >> $WG_CONF 55 | echo "ListenPort = $WG_PORT" >> $WG_CONF 56 | echo "PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE" >> $WG_CONF 57 | echo "PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE" >> $WG_CONF 58 | } 59 | 60 | enable_service() { 61 | systemctl enable wg-quick@$WG_ID.service 62 | systemctl start wg-quick@$WG_ID.service 63 | systemctl status wg-quick@$WG_ID.service 64 | } 65 | 66 | restart_service() { 67 | wg syncconf "$WG_ID" <(wg-quick strip "$WG_ID") 68 | } 69 | 70 | enable_ip_forwarwing() { 71 | test -z "$(grep "net.ipv4.ip_forward=1" /etc/sysctl.conf)" && \ 72 | echo -e "net.ipv4.ip_forward=1\nnet.ipv6.conf.all.forwarding = 1" >> /etc/sysctl.conf && sysctl -p 73 | } 74 | 75 | get_global_ipv4() { 76 | ip -4 addr | sed -ne 's|^.* inet \([^/]*\)/.* scope global.*$|\1|p' | awk '{print $1}' | head -1 77 | } 78 | 79 | is_exists_client_config() { 80 | test -e "$WG_PEERS/$1.conf" || bye "Peer '$(basename "$1")' does not exists" 10 81 | } 82 | 83 | validate_client_name() { 84 | [[ "$1" =~ ^[a-zA-Z0-9_-]+$ ]] || bye "Wrong client name '$1'" 11 85 | } 86 | 87 | add_client() { 88 | test -e "$WG_PEERS/$1.conf" && bye "Peer '$1' already exists" 12 89 | validate_client_name "$1" 90 | client_private_key=$(wg genkey) 91 | client_public_key=$(echo "$client_private_key" | wg pubkey) 92 | client_psk=$(wg genpsk) 93 | most_recent_client_ip=$(grep -A 3 '\[Peer\]' $WG_CONF | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | awk -F '.' '{print $4}' | sort -nr | head -n 1) 94 | 95 | test "$most_recent_client_ip" -gt 253 && bye "Only 253 peers" 24 # 24 subnet 96 | 97 | # most recent client address + 1 98 | test -z "$most_recent_client_ip" && client_ip=$WG_SUBNET"2" || client_ip=$WG_SUBNET$(("$most_recent_client_ip" + 1)) 99 | echo -e "\n# Client $1\n[Peer]\nPublicKey = $client_public_key\nPresharedKey = $client_psk\nAllowedIPs = $client_ip/32" >> $WG_CONF 100 | restart_service 101 | 102 | echo "[Interface] 103 | PrivateKey = $client_private_key 104 | Address = $client_ip/32 105 | DNS = $CLIENT_DNSs 106 | 107 | [Peer] 108 | PublicKey = $SERVER_PUBLIC_KEY 109 | PresharedKey = $client_psk 110 | Endpoint = $(get_global_ipv4):$WG_PORT 111 | AllowedIPs = 0.0.0.0/0, ::/0 112 | PersistentKeepalive = 20" | tee $WG_PEERS/"$1".conf 113 | } 114 | 115 | delete_client() { 116 | is_exists_client_config "$1" 117 | rm "$WG_PEERS/$1.conf" 118 | sed -i "/^# Client $1\$/,/^$/d" "$WG_CONF" 119 | restart_service 120 | } 121 | 122 | get_client() { 123 | is_exists_client_config "$1" 124 | cat $WG_PEERS/"$1".conf 125 | } 126 | 127 | get_clients_names() { 128 | ls "$WG_PEERS" | awk -F "." '{print $1}' 129 | } 130 | 131 | get_client_qrcode_png() { 132 | is_exists_client_config "$1" 133 | qrencode -l L -o - -r $WG_PEERS/"$1".conf 134 | } 135 | 136 | show_client_qrcode() { 137 | is_exists_client_config "$1" 138 | qrencode -t ansiutf8 -l L -r $WG_PEERS/"$1".conf | less -R 139 | } 140 | 141 | cmd_add_client() { 142 | add_client "$1" >/dev/null 143 | show_client_qrcode "$1" | less -R 144 | echo "It also available in $WG_PEERS/$1.conf" 145 | } 146 | 147 | cmd_add_client_and_get_client_qrcode_png() { 148 | add_client "$1" >/dev/null 149 | get_client_qrcode_png "$1" 150 | } 151 | 152 | cmd_usage() { 153 | echo "Usage $0" 154 | echo -e "\tinit - Initialize wireguard" 155 | echo -e "\tadd - add client and show qrcode" 156 | echo -e "\tget - show qrcode client " 157 | echo -e "\trm - remove client" 158 | echo -e "\tls - list clients" 159 | echo -e "\tget_tg - get client qrcode in bytes" 160 | echo -e "\tget_file - get client config file" 161 | echo -e "\tadd_tg - add client and get qrcode in bytes" 162 | } 163 | 164 | cmd_init() { 165 | check_all 166 | mkdir -p $WG_PEERS 167 | chmod 600 -R $WG_PREFIX 168 | generate_server_keys 169 | write_params 170 | create_config_file 171 | enable_ip_forwarwing 172 | enable_service 173 | add_client "initial" >/dev/null 174 | } 175 | 176 | test -e && source $WG_PARAMS 177 | case "$1" in 178 | usage) shift; cmd_usage "$@" ;; 179 | init) shift; cmd_init "$@" ;; 180 | add) shift; cmd_add_client "$@" ;; 181 | get) shift; show_client_qrcode "$@" ;; 182 | rm) shift; delete_client "$@" ;; 183 | ls) shift; get_clients_names "$@" ;; 184 | get_tg) shift; get_client_qrcode_png "$@" ;; 185 | get_file) shift; get_client "$@" ;; 186 | add_tg) shift; cmd_add_client_and_get_client_qrcode_png "$@" ;; 187 | "") shift; bye "No command, use '$0 usage'" 1 ;; 188 | *) bye "Wrong command '$1', use '$0 usage'" 1 ;; 189 | esac 190 | exit 0 191 | -------------------------------------------------------------------------------- /bot/handlers/get_client.py: -------------------------------------------------------------------------------- 1 | import math 2 | from aiogram.dispatcher import FSMContext 3 | from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup 4 | 5 | from data import Text, ButtonText 6 | from keyboards import cancel, menu 7 | from loader import dp, logger 8 | from shell_interface import get_clients_from_manager, get_config_qrcode, put_bytes_to_file, get_config_raw, \ 9 | delete_client, raw_to_html 10 | from states import GetClient 11 | 12 | 13 | NUMBER_OF_CLIENTS_ON_PAGE = 6 14 | 15 | 16 | def get_clients_with_offset_fill_blank_clients(clients, page: int): 17 | offset = NUMBER_OF_CLIENTS_ON_PAGE * page 18 | 19 | res = clients[offset:(NUMBER_OF_CLIENTS_ON_PAGE + offset)] 20 | 21 | for _ in range(NUMBER_OF_CLIENTS_ON_PAGE - len(res)): 22 | res.append(" " * 20) 23 | return res 24 | 25 | 26 | @dp.callback_query_handler(text_contains="cancel", state=GetClient) 27 | async def cancel_order(call: CallbackQuery, state: FSMContext): 28 | await call.message.edit_text(Text.MENU, reply_markup=menu) 29 | await call.answer() 30 | await state.finish() 31 | 32 | 33 | @dp.callback_query_handler(text_contains="cancel") 34 | async def cancel_order2(call: CallbackQuery): 35 | await call.message.edit_text(Text.MENU, reply_markup=menu) 36 | await call.answer() 37 | 38 | 39 | @dp.callback_query_handler(text="_") 40 | async def plug(call: CallbackQuery): 41 | await call.answer() 42 | 43 | 44 | @dp.callback_query_handler(text_contains="clients") 45 | async def get_client(call: CallbackQuery, state: FSMContext): 46 | page = int(call.data.split(":")[1]) 47 | 48 | all_clients = get_clients_from_manager() 49 | 50 | total_pages = math.ceil(len(all_clients) / NUMBER_OF_CLIENTS_ON_PAGE) 51 | 52 | if page >= total_pages: 53 | page = 0 54 | 55 | if page < 0: 56 | page = total_pages - 1 57 | 58 | clients = get_clients_with_offset_fill_blank_clients(all_clients, page) 59 | 60 | clients_keyboard = InlineKeyboardMarkup() 61 | for client_name in clients: 62 | if client_name == " " * 20: 63 | clients_keyboard.insert(InlineKeyboardButton(text=client_name, callback_data=f'_')) 64 | else: 65 | clients_keyboard.insert(InlineKeyboardButton(text=client_name, callback_data=f'client_name:{client_name}:{page}')) 66 | 67 | prev_page_button = InlineKeyboardButton(text="<", callback_data=f"clients:{page - 1}") 68 | current_page_button = InlineKeyboardButton(text=f"{page+1}/{total_pages}", callback_data="_") 69 | next_page_button = InlineKeyboardButton(text=">", callback_data=f"clients:{page + 1}") 70 | 71 | clients_keyboard.row(prev_page_button, current_page_button, next_page_button) 72 | clients_keyboard.row(cancel) 73 | 74 | await call.message.edit_text(Text.CLIENTS, reply_markup=clients_keyboard) 75 | await call.answer() 76 | 77 | 78 | @dp.callback_query_handler(text_contains="client_name") 79 | async def get_client_2(call: CallbackQuery, state: FSMContext): 80 | client_name = call.data.split(':')[1] 81 | page = call.data.split(':')[2] 82 | 83 | get_client_menu = InlineKeyboardMarkup( 84 | inline_keyboard=[ 85 | [ 86 | InlineKeyboardButton(text=ButtonText.GET_QR, callback_data=f'get_client_get_name:get_qrcode:{client_name}'), 87 | InlineKeyboardButton(text=ButtonText.GET_FILE, callback_data=f'get_client_get_name:get_file:{client_name}'), 88 | ], 89 | [ 90 | InlineKeyboardButton(text=ButtonText.GET_RAW, callback_data=f"get_client_get_name:get_raw:{client_name}"), 91 | ], 92 | [ 93 | InlineKeyboardButton(text=ButtonText.DELETE, callback_data=f"get_client_get_name:delete:{client_name}"), 94 | ], 95 | [ 96 | InlineKeyboardButton(text=ButtonText.BACK_MENU, callback_data=f"clients:{page}") 97 | ] 98 | ] 99 | ) 100 | 101 | await call.message.edit_text(Text.CLIENT.format(client_name=client_name), reply_markup=get_client_menu) 102 | await call.answer() 103 | 104 | 105 | @dp.callback_query_handler(text_contains="get_client_get_name") 106 | async def get_client_3(call: CallbackQuery, state: FSMContext): 107 | _, command, client_name = call.data.split(":") 108 | 109 | if command == "get_qrcode": 110 | photo = put_bytes_to_file(get_config_qrcode(client_name)) 111 | photo.name = client_name + ".png" 112 | await call.message.answer_photo(photo=photo) 113 | await call.answer() 114 | logger.info(f"get qrcode \"{client_name}\" from user {call.from_user.username}:{call.from_user.id}") 115 | elif command == "get_file": 116 | document = put_bytes_to_file(get_config_raw(client_name)) 117 | document.name = client_name + ".conf" 118 | await call.message.answer_document(document=document) 119 | await call.answer() 120 | logger.info(f"get file \"{client_name}\" from user {call.from_user.username}:{call.from_user.id}") 121 | elif command == "get_raw": 122 | await call.message.answer(raw_to_html(get_config_raw(client_name).decode()), parse_mode="html") 123 | await call.answer() 124 | logger.info(f"get raw \"{client_name}\" from user {call.from_user.username}:{call.from_user.id}") 125 | elif command == "delete": 126 | conf_del = InlineKeyboardMarkup( 127 | inline_keyboard=[ 128 | [ 129 | InlineKeyboardButton(text=ButtonText.CONFIRM, callback_data=f"confirm:{client_name}") 130 | ], 131 | [ 132 | InlineKeyboardButton(text=ButtonText.BACK_MENU, callback_data="cancel:_") 133 | ], 134 | ] 135 | ) 136 | await call.answer() 137 | await call.message.edit_text(Text.CLIENT_DELETE_CONFIRM.format(client_name=client_name), reply_markup=conf_del) 138 | await GetClient.name.set() 139 | 140 | 141 | @dp.callback_query_handler(state=GetClient.name) 142 | async def get_client_4(call: CallbackQuery, state: FSMContext): 143 | _, client_name = call.data.split(":") 144 | delete_client(client_name) 145 | await call.message.edit_text(Text.CLIENT_DELETED.format(client_name=client_name), reply_markup=menu) 146 | await call.answer() 147 | await state.finish() 148 | logger.info(f"deleted client \"{client_name}\" from user {call.from_user.username}:{call.from_user.id}") 149 | --------------------------------------------------------------------------------