правки
This commit is contained in:
@@ -3,7 +3,6 @@ package screens
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
@@ -33,10 +32,16 @@ type SelectChannelsForPurchase struct {
|
||||
Channels []PurchaseChannelInput
|
||||
CurrentPage int
|
||||
InvalidUsernames []string
|
||||
Duplicates []string // дубликаты каналов
|
||||
ParsingErrors []ParseError // ошибки парсинга с предложениями
|
||||
BackState bot.State
|
||||
}
|
||||
|
||||
var channelUsernameRe = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{4,31}$`)
|
||||
// ParseError представляет ошибку парсинга с предложением исправления
|
||||
type ParseError struct {
|
||||
Input string
|
||||
Suggestion string
|
||||
}
|
||||
|
||||
func (s *SelectChannelsForPurchase) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
s.renderChannelSelection(b, mode)
|
||||
@@ -118,6 +123,30 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
|
||||
s.InvalidUsernames = nil
|
||||
}
|
||||
|
||||
// Показываем дубликаты
|
||||
if len(s.Duplicates) > 0 {
|
||||
text += fmt.Sprintf("\n\n⏭️ Пропущены (дубликаты): %s", strings.Join(s.Duplicates, ", "))
|
||||
s.Duplicates = nil
|
||||
}
|
||||
|
||||
// Показываем ошибки с предложениями
|
||||
if len(s.ParsingErrors) > 0 {
|
||||
text += "\n\n❌ Ошибки форматирования:\n"
|
||||
for i, err := range s.ParsingErrors {
|
||||
if i < 3 { // Показываем максимум 3 ошибки
|
||||
if err.Suggestion != "" {
|
||||
text += fmt.Sprintf("• %s → возможно: %s\n", err.Input, err.Suggestion)
|
||||
} else {
|
||||
text += fmt.Sprintf("• %s\n", err.Input)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(s.ParsingErrors) > 3 {
|
||||
text += fmt.Sprintf("• ... и еще %d\n", len(s.ParsingErrors)-3)
|
||||
}
|
||||
s.ParsingErrors = nil
|
||||
}
|
||||
|
||||
// Кнопки навигации
|
||||
if len(s.Channels) > 0 {
|
||||
buttons = append(buttons, Row(
|
||||
@@ -164,18 +193,18 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем/обновляем каналы в БД через API
|
||||
jwt := b.Session.JWT
|
||||
if jwt == "" {
|
||||
log.Error().Msg("JWT is empty in session")
|
||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
|
||||
return
|
||||
// Проверяем, что все каналы резолвлены (имеют ChannelID)
|
||||
var unresolved []string
|
||||
for _, ch := range s.Channels {
|
||||
if ch.ChannelID == "" {
|
||||
label := channelLabel(ch)
|
||||
unresolved = append(unresolved, label)
|
||||
}
|
||||
}
|
||||
|
||||
failed := s.resolveChannels(b, jwt)
|
||||
if len(failed) > 0 {
|
||||
text := "❌ Не удалось добавить каналы:\n\n"
|
||||
for _, label := range failed {
|
||||
if len(unresolved) > 0 {
|
||||
text := "❌ Некоторые каналы не были найдены:\n\n"
|
||||
for _, label := range unresolved {
|
||||
text += fmt.Sprintf("• %s\n", label)
|
||||
}
|
||||
text += "\nУдалите их из списка и попробуйте снова"
|
||||
@@ -186,6 +215,7 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
|
||||
return
|
||||
}
|
||||
|
||||
// Каналы уже резолвлены в HandleMessage, переходим к следующему шагу
|
||||
b.SetState(&PurchaseOptionalDetails{
|
||||
ProjectID: s.ProjectID,
|
||||
ProjectTitle: s.ProjectTitle,
|
||||
@@ -290,12 +320,25 @@ func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем JWT
|
||||
jwt := b.Session.JWT
|
||||
if jwt == "" {
|
||||
log.Error().Msg("JWT is empty in session")
|
||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
|
||||
return
|
||||
}
|
||||
|
||||
// Разбиваем ввод на токены
|
||||
tokens := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
return r == ' ' || r == '\n' || r == '\t' || r == ',' || r == ';'
|
||||
})
|
||||
|
||||
added := 0
|
||||
var invalid []string
|
||||
s.Duplicates = nil
|
||||
s.InvalidUsernames = nil
|
||||
s.ParsingErrors = nil
|
||||
|
||||
tokensLoop:
|
||||
for _, token := range tokens {
|
||||
entry := strings.TrimSpace(token)
|
||||
entry = strings.Trim(entry, ",;")
|
||||
@@ -303,53 +346,144 @@ func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update
|
||||
continue
|
||||
}
|
||||
|
||||
var username string
|
||||
var inviteLink string
|
||||
if isInviteLink(entry) {
|
||||
inviteLink = entry
|
||||
} else {
|
||||
username = strings.TrimPrefix(entry, "@")
|
||||
username = strings.Trim(username, "@")
|
||||
if !channelUsernameRe.MatchString(username) {
|
||||
invalid = append(invalid, entry)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Используем новый парсер
|
||||
parsed := ParseChannelInput(entry)
|
||||
|
||||
exists := false
|
||||
for _, ch := range s.Channels {
|
||||
if inviteLink != "" && ch.InviteLink == inviteLink {
|
||||
exists = true
|
||||
break
|
||||
if !parsed.Valid {
|
||||
// Пробуем предложить исправление
|
||||
if suggestion, ok := SuggestFix(entry); ok {
|
||||
s.ParsingErrors = append(s.ParsingErrors, ParseError{
|
||||
Input: entry,
|
||||
Suggestion: suggestion,
|
||||
})
|
||||
} else {
|
||||
s.InvalidUsernames = append(s.InvalidUsernames, entry)
|
||||
}
|
||||
if username != "" && ch.Username == username {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Проверяем дубликаты по username/invite link
|
||||
if IsDuplicate(parsed, s.Channels) {
|
||||
label := FormatChannelLabel(parsed)
|
||||
s.Duplicates = append(s.Duplicates, label)
|
||||
continue
|
||||
}
|
||||
|
||||
// Сразу резолвим канал через бэкенд
|
||||
channel, err := s.resolveSingleChannel(b, jwt, parsed)
|
||||
if err != nil {
|
||||
// Канал не найден или ошибка
|
||||
s.InvalidUsernames = append(s.InvalidUsernames, entry)
|
||||
log.Error().Err(err).Str("input", entry).Msg("Failed to resolve channel")
|
||||
continue
|
||||
}
|
||||
|
||||
// Проверяем дубликаты по ID канала (после резолва)
|
||||
for _, ch := range s.Channels {
|
||||
if ch.ChannelID != "" && ch.ChannelID == channel.ID {
|
||||
var title, username string
|
||||
if channel.Title != nil {
|
||||
title = *channel.Title
|
||||
}
|
||||
if channel.Username != nil {
|
||||
username = *channel.Username
|
||||
}
|
||||
label := channelLabelByID(&channel.ID, &title, &username)
|
||||
s.Duplicates = append(s.Duplicates, label)
|
||||
continue tokensLoop // <-- Выход из внешнего цикла, а не внутреннего!
|
||||
}
|
||||
|
||||
// Дополнительная проверка по username (case-insensitive)
|
||||
// На случай, если канал еще не резолвлен или ID отличается
|
||||
if channel.Username != nil && ch.Username != "" {
|
||||
if strings.EqualFold(ch.Username, *channel.Username) {
|
||||
var title string
|
||||
if channel.Title != nil {
|
||||
title = *channel.Title
|
||||
}
|
||||
label := channelLabelByID(&channel.ID, &title, channel.Username)
|
||||
s.Duplicates = append(s.Duplicates, label)
|
||||
continue tokensLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем канал с полными данными
|
||||
var title string
|
||||
if channel.Title != nil {
|
||||
title = *channel.Title
|
||||
}
|
||||
s.Channels = append(s.Channels, PurchaseChannelInput{
|
||||
Username: username,
|
||||
InviteLink: inviteLink,
|
||||
ChannelID: channel.ID,
|
||||
Username: parsed.Username,
|
||||
Title: title,
|
||||
InviteLink: parsed.InviteLink,
|
||||
})
|
||||
added++
|
||||
}
|
||||
|
||||
if added == 0 {
|
||||
if len(invalid) == 0 {
|
||||
return
|
||||
}
|
||||
// Если ничего не добавлено и нет ошибок - не обновляем экран
|
||||
if added == 0 && len(s.InvalidUsernames) == 0 && len(s.Duplicates) == 0 && len(s.ParsingErrors) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
s.InvalidUsernames = invalid
|
||||
|
||||
// Обновляем экран
|
||||
s.Enter(b, bot.NewMessage)
|
||||
}
|
||||
|
||||
// resolveSingleChannel резолвит один канал через бэкенд
|
||||
func (s *SelectChannelsForPurchase) resolveSingleChannel(b *bot.Bot, jwt string, parsed ChannelInput) (*backend.Channel, error) {
|
||||
var input backend.CreateChannelInput
|
||||
if parsed.InviteLink != "" {
|
||||
link := parsed.InviteLink
|
||||
input = backend.CreateChannelInput{InviteLink: &link}
|
||||
} else {
|
||||
username := parsed.Username
|
||||
input = backend.CreateChannelInput{Username: &username}
|
||||
}
|
||||
|
||||
resp, err := b.Backend.CreateChannels(context.Background(), jwt, backend.CreateChannelsInput{
|
||||
Channels: []backend.CreateChannelInput{input},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.Results) == 0 {
|
||||
return nil, fmt.Errorf("no response from backend")
|
||||
}
|
||||
|
||||
result := resp.Results[0]
|
||||
if result.Status == "failed" || result.Channel == nil || result.Channel.ID == "" {
|
||||
errMsg := "channel not found"
|
||||
if result.Error != nil {
|
||||
errMsg = *result.Error
|
||||
}
|
||||
return nil, fmt.Errorf(errMsg)
|
||||
}
|
||||
|
||||
// Если пришел username из ответа, обновляем его
|
||||
if result.Channel.Username != nil && *result.Channel.Username != "" {
|
||||
parsed.Username = *result.Channel.Username
|
||||
}
|
||||
|
||||
return result.Channel, nil
|
||||
}
|
||||
|
||||
// channelLabelByID формирует метку канала по ID
|
||||
func channelLabelByID(id, title, username *string) string {
|
||||
if title != nil && *title != "" {
|
||||
return *title
|
||||
}
|
||||
if username != nil && *username != "" {
|
||||
return "@" + *username
|
||||
}
|
||||
if id != nil && *id != "" {
|
||||
return *id
|
||||
}
|
||||
return "канал"
|
||||
}
|
||||
|
||||
func (s *SelectChannelsForPurchase) resolveChannels(b *bot.Bot, jwt string) []string {
|
||||
inputs := make([]backend.CreateChannelInput, 0, len(s.Channels))
|
||||
for _, ch := range s.Channels {
|
||||
|
||||
Reference in New Issue
Block a user