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.
Publish telemt-bot Docker image / build-and-push (push) Successful in 50s

This commit is contained in:
Denozordec
2026-03-22 12:17:00 +07:00
parent f288667d82
commit fdaa849494
+161 -29
View File
@@ -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, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
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("<b>Пользователи</b>")
sb.WriteString("\n")
sb.WriteString(fmt.Sprintf("<i>Страница %d из %d · всего записей: %d</i>", page+1, totalPages, total))
sb.WriteString("\n\n")
for i := start; i < end; i++ {
u := users[i]
globalIdx := i + 1
sb.WriteString(fmt.Sprintf("<b>%d.</b> <code>%s</code>\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,
" соединений: <b>%d</b> · уник. IP: <b>%d</b> · трафик: <b>%s</b>\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)
}