├── 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 |
--------------------------------------------------------------------------------