From fdaa8494949b0a638b355166e046fd303d4c6ce9 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 22 Mar 2026 12:17:00 +0700 Subject: [PATCH] Refactor user handling in main.go to implement pagination for user lists. Introduce new functions for building user list messages and handling callback queries for page navigation. Enhance message formatting with HTML support for improved user experience. --- main.go | 190 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 161 insertions(+), 29 deletions(-) diff --git a/main.go b/main.go index d40d37b..07542f9 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,9 @@ const ( defaultTelemtTimeoutSeconds = 5 defaultTelegramTimeoutSeconds = 60 defaultListenPollTimeout = 50 + + usersPerPage = 12 + usersPageCbPrefix = "u_pg:" ) type config struct { @@ -448,23 +451,8 @@ func (b *bot) handleUsers(ctx context.Context, chatID int64) error { return b.sendMessage(ctx, chatID, "Пользователи не найдены.") } - const maxRows = 20 - var sb strings.Builder - if len(users) > maxRows { - sb.WriteString(fmt.Sprintf("Показаны первые %d из %d\n", maxRows, len(users))) - } - limit := len(users) - if limit > maxRows { - limit = maxRows - } - for i := 0; i < limit; i++ { - u := users[i] - sb.WriteString(fmt.Sprintf( - "%d) %s | conn=%d | ips=%d | octets=%d\n", - i+1, u.Username, u.CurrentConnections, u.ActiveUniqueIPs, u.TotalOctets, - )) - } - return b.sendMessage(ctx, chatID, strings.TrimRight(sb.String(), "\n")) + text, kb := buildUsersListMessage(users, 0) + return b.sendMessageWithKeyboardHTML(ctx, chatID, text, kb, true) } func (b *bot) handleCreateUser(ctx context.Context, chatID int64, text string) error { @@ -670,6 +658,17 @@ func (b *bot) handleCallbackQuery(ctx context.Context, cq *tgCallbackQuery) erro delete(b.awaitingUsername, chatID) return b.editMessageText(ctx, chatID, msgID, "Выберите действие:", mainMenuKeyboard()) default: + if strings.HasPrefix(cq.Data, usersPageCbPrefix) { + rest := strings.TrimPrefix(cq.Data, usersPageCbPrefix) + page, err := strconv.Atoi(rest) + if err != nil || page < 0 { + return b.answerCallbackQuery(ctx, cq.ID, "Некорректная страница", true) + } + if err := b.answerCallbackQuery(ctx, cq.ID, "", false); err != nil { + return err + } + return b.handleUsersPageCB(ctx, chatID, msgID, page) + } return b.answerCallbackQuery(ctx, cq.ID, "Неизвестное действие", false) } } @@ -696,6 +695,10 @@ func (b *bot) handleSummaryCB(ctx context.Context, chatID, msgID int64) error { } func (b *bot) handleUsersCB(ctx context.Context, chatID, msgID int64) error { + return b.handleUsersPageCB(ctx, chatID, msgID, 0) +} + +func (b *bot) handleUsersPageCB(ctx context.Context, chatID, msgID int64, page int) error { var users []userInfo if err := b.callTelemt(ctx, "/v1/stats/users", &users); err != nil { return b.editMessageText(ctx, chatID, msgID, "Ошибка API: "+err.Error(), backKeyboard()) @@ -705,23 +708,123 @@ func (b *bot) handleUsersCB(ctx context.Context, chatID, msgID int64) error { return b.editMessageText(ctx, chatID, msgID, "Пользователи не найдены.", backKeyboard()) } - const maxRows = 20 + totalPages := usersTotalPages(len(users)) + if page >= totalPages { + page = totalPages - 1 + } + if page < 0 { + page = 0 + } + + text, kb := buildUsersListMessage(users, page) + return b.editMessageTextHTML(ctx, chatID, msgID, text, kb, true) +} + +func usersTotalPages(n int) int { + if n == 0 { + return 1 + } + pages := n / usersPerPage + if n%usersPerPage != 0 { + pages++ + } + if pages < 1 { + return 1 + } + return pages +} + +func escapeHTML(s string) string { + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, "<", "<") + s = strings.ReplaceAll(s, ">", ">") + return s +} + +// formatTrafficRu форматирует объём в октетах (байтах) в удобочитаемый вид (двоичные приставки). +func formatTrafficRu(octets uint64) string { + if octets == 0 { + return "0 Б" + } + const ( + kib = 1024 + mib = kib * 1024 + gib = mib * 1024 + tib = gib * 1024 + ) + switch { + case octets >= tib: + return fmt.Sprintf("%.2f ТиБ", float64(octets)/float64(tib)) + case octets >= gib: + return fmt.Sprintf("%.2f ГиБ", float64(octets)/float64(gib)) + case octets >= mib: + return fmt.Sprintf("%.2f МиБ", float64(octets)/float64(mib)) + case octets >= kib: + return fmt.Sprintf("%.1f КиБ", float64(octets)/float64(kib)) + default: + return fmt.Sprintf("%d Б", octets) + } +} + +func buildUsersListMessage(users []userInfo, page int) (string, *inlineKeyboardMarkup) { + total := len(users) + totalPages := usersTotalPages(total) + start := page * usersPerPage + if start > total { + start = 0 + } + end := start + usersPerPage + if end > total { + end = total + } + var sb strings.Builder - if len(users) > maxRows { - sb.WriteString(fmt.Sprintf("Показаны первые %d из %d\n", maxRows, len(users))) - } - limit := len(users) - if limit > maxRows { - limit = maxRows - } - for i := 0; i < limit; i++ { + sb.WriteString("Пользователи") + sb.WriteString("\n") + sb.WriteString(fmt.Sprintf("Страница %d из %d · всего записей: %d", page+1, totalPages, total)) + sb.WriteString("\n\n") + + for i := start; i < end; i++ { u := users[i] + globalIdx := i + 1 + sb.WriteString(fmt.Sprintf("%d. %s\n", globalIdx, escapeHTML(u.Username))) sb.WriteString(fmt.Sprintf( - "%d) %s | conn=%d | ips=%d | octets=%d\n", - i+1, u.Username, u.CurrentConnections, u.ActiveUniqueIPs, u.TotalOctets, + " соединений: %d · уник. IP: %d · трафик: %s\n", + u.CurrentConnections, u.ActiveUniqueIPs, formatTrafficRu(u.TotalOctets), )) + if i+1 < end { + sb.WriteString("\n") + } } - return b.editMessageText(ctx, chatID, msgID, strings.TrimRight(sb.String(), "\n"), backKeyboard()) + + kb := usersListKeyboard(page, totalPages) + return strings.TrimRight(sb.String(), "\n"), kb +} + +func usersListKeyboard(page, totalPages int) *inlineKeyboardMarkup { + var rows [][]inlineKeyboardButton + var nav []inlineKeyboardButton + if page > 0 { + nav = append(nav, inlineKeyboardButton{ + Text: "◀ Назад", + CallbackData: usersPageCbPrefix + strconv.Itoa(page-1), + Style: "primary", + }) + } + if page < totalPages-1 { + nav = append(nav, inlineKeyboardButton{ + Text: "Вперёд ▶", + CallbackData: usersPageCbPrefix + strconv.Itoa(page+1), + Style: "primary", + }) + } + if len(nav) > 0 { + rows = append(rows, nav) + } + rows = append(rows, []inlineKeyboardButton{ + {Text: "« В меню", CallbackData: "cmd_menu"}, + }) + return &inlineKeyboardMarkup{InlineKeyboard: rows} } func (b *bot) handleCreateUserDirect(ctx context.Context, chatID int64, username string) error { @@ -849,6 +952,15 @@ func (b *bot) sendMessage(ctx context.Context, chatID int64, text string) error } func (b *bot) sendMessageWithKeyboard(ctx context.Context, chatID int64, text string, keyboard *inlineKeyboardMarkup) error { + return b.sendMessageWithKeyboardOpts(ctx, chatID, text, keyboard, "", false) +} + +// sendMessageWithKeyboardHTML — сообщение с разметкой HTML (см. Bot API: formatting options, link_preview_options). +func (b *bot) sendMessageWithKeyboardHTML(ctx context.Context, chatID int64, text string, keyboard *inlineKeyboardMarkup, disableLinkPreview bool) error { + return b.sendMessageWithKeyboardOpts(ctx, chatID, text, keyboard, "HTML", disableLinkPreview) +} + +func (b *bot) sendMessageWithKeyboardOpts(ctx context.Context, chatID int64, text string, keyboard *inlineKeyboardMarkup, parseMode string, disableLinkPreview bool) error { reqBody := map[string]any{ "chat_id": chatID, "text": text, @@ -856,10 +968,24 @@ func (b *bot) sendMessageWithKeyboard(ctx context.Context, chatID int64, text st if keyboard != nil { reqBody["reply_markup"] = keyboard } + if parseMode != "" { + reqBody["parse_mode"] = parseMode + } + if disableLinkPreview { + reqBody["link_preview_options"] = map[string]bool{"is_disabled": true} + } return b.postTelegram(ctx, "/sendMessage", reqBody) } func (b *bot) editMessageText(ctx context.Context, chatID, messageID int64, text string, keyboard *inlineKeyboardMarkup) error { + return b.editMessageTextOpts(ctx, chatID, messageID, text, keyboard, "", false) +} + +func (b *bot) editMessageTextHTML(ctx context.Context, chatID, messageID int64, text string, keyboard *inlineKeyboardMarkup, disableLinkPreview bool) error { + return b.editMessageTextOpts(ctx, chatID, messageID, text, keyboard, "HTML", disableLinkPreview) +} + +func (b *bot) editMessageTextOpts(ctx context.Context, chatID, messageID int64, text string, keyboard *inlineKeyboardMarkup, parseMode string, disableLinkPreview bool) error { reqBody := map[string]any{ "chat_id": chatID, "message_id": messageID, @@ -868,6 +994,12 @@ func (b *bot) editMessageText(ctx context.Context, chatID, messageID int64, text if keyboard != nil { reqBody["reply_markup"] = keyboard } + if parseMode != "" { + reqBody["parse_mode"] = parseMode + } + if disableLinkPreview { + reqBody["link_preview_options"] = map[string]bool{"is_disabled": true} + } return b.postTelegram(ctx, "/editMessageText", reqBody) }