приватные каналы

This commit is contained in:
Artem Tsyrulnikov
2026-01-21 02:41:20 +03:00
parent 233afb5451
commit b5695360c2
27 changed files with 1026 additions and 217 deletions

View File

@@ -14,7 +14,10 @@ import (
)
type PurchaseChannelInput struct {
ChannelID string
Username string
Title string
InviteLink string
PlannedCost *float64
Comment *string
}
@@ -48,15 +51,15 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
if len(s.Channels) == 0 {
text += `Добавьте каналы для размещения рекламы
Отправьте username канала (без @)
Например: <code>channel_name</code>
Отправьте username (без @) или invite link приватного канала
Например: <code>channel_name</code> или <code>https://t.me/+abcdef</code>
После добавления всех каналов нажмите <b>Далее</b>`
} else {
text += fmt.Sprintf("Добавлено каналов: <b>%d</b>\n\n", len(s.Channels))
for i, ch := range s.Channels {
channelText := fmt.Sprintf(" %d. @%s", i+1, ch.Username)
channelText := fmt.Sprintf(" %d. %s", i+1, channelLabel(ch))
if ch.PlannedCost != nil {
channelText += fmt.Sprintf(" — %.0f₽", *ch.PlannedCost)
}
@@ -73,9 +76,9 @@ func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.
var slots []echotron.InlineKeyboardButton
for i := start; i < end; i++ {
ch := s.Channels[i]
buttonText := fmt.Sprintf("@%s", ch.Username)
buttonText := channelLabel(ch)
if ch.PlannedCost != nil {
buttonText = fmt.Sprintf("@%s — %.0f₽", ch.Username, *ch.PlannedCost)
buttonText = fmt.Sprintf("%s — %.0f₽", channelLabel(ch), *ch.PlannedCost)
}
slots = append(slots, Button(buttonText, fmt.Sprintf("remove_channel:%d", i)))
}
@@ -161,7 +164,7 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
return
}
// Проверяем существование каналов через API
// Создаем/обновляем каналы в БД через API
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
@@ -169,11 +172,11 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
return
}
notFoundChannels := s.validateChannels(b, jwt)
if len(notFoundChannels) > 0 {
text := "❌ Следующие каналы не найдены:\n\n"
for _, username := range notFoundChannels {
text += fmt.Sprintf("• @%s\n", username)
failed := s.resolveChannels(b, jwt)
if len(failed) > 0 {
text := "❌ Не удалось добавить каналы:\n\n"
for _, label := range failed {
text += fmt.Sprintf("• %s\n", label)
}
text += "\nУдалите их из списка и попробуйте снова"
@@ -242,9 +245,9 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
}
}
apiChannels = append(apiChannels, backend.CreatePlacementChannelInput{
Username: ch.Username,
Comment: ch.Comment,
Details: details,
ChannelID: ch.ChannelID,
Comment: ch.Comment,
Details: details,
})
}
@@ -294,22 +297,32 @@ func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update
added := 0
var invalid []string
for _, token := range tokens {
username := strings.TrimSpace(token)
username = strings.TrimPrefix(username, "@")
username = strings.Trim(username, "@")
username = strings.Trim(username, ",;")
if username == "" {
entry := strings.TrimSpace(token)
entry = strings.Trim(entry, ",;")
if entry == "" {
continue
}
if !channelUsernameRe.MatchString(username) {
invalid = append(invalid, username)
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
}
}
exists := false
for _, ch := range s.Channels {
if ch.Username == username {
if inviteLink != "" && ch.InviteLink == inviteLink {
exists = true
break
}
if username != "" && ch.Username == username {
exists = true
break
}
@@ -319,7 +332,8 @@ func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update
}
s.Channels = append(s.Channels, PurchaseChannelInput{
Username: username,
Username: username,
InviteLink: inviteLink,
})
added++
}
@@ -336,37 +350,48 @@ func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update
s.Enter(b, bot.NewMessage)
}
func (s *SelectChannelsForPurchase) validateChannels(b *bot.Bot, jwt string) []string {
var notFound []string
func (s *SelectChannelsForPurchase) resolveChannels(b *bot.Bot, jwt string) []string {
inputs := make([]backend.CreateChannelInput, 0, len(s.Channels))
for _, ch := range s.Channels {
channels, err := b.Backend.SearchChannels(
context.Background(),
jwt,
ch.Username,
)
if err != nil {
log.Error().Err(err).Str("username", ch.Username).Msg("Failed to search channel")
notFound = append(notFound, ch.Username)
continue
}
// Проверяем что нашли хотя бы один канал с точным совпадением username
found := false
for _, channel := range channels {
if channel.Username != nil && *channel.Username == ch.Username {
found = true
break
}
}
if !found {
notFound = append(notFound, ch.Username)
if ch.InviteLink != "" {
link := ch.InviteLink
inputs = append(inputs, backend.CreateChannelInput{InviteLink: &link})
} else {
username := ch.Username
inputs = append(inputs, backend.CreateChannelInput{Username: &username})
}
}
return notFound
resp, err := b.Backend.CreateChannels(context.Background(), jwt, backend.CreateChannelsInput{
Channels: inputs,
})
if err != nil {
log.Error().Err(err).Msg("Failed to create channels")
return []string{"ошибка сервера"}
}
var failed []string
for _, result := range resp.Results {
if result.Status == "failed" || result.Channel == nil || result.Channel.ID == "" {
if result.Index >= 0 && result.Index < len(s.Channels) {
failed = append(failed, channelLabel(s.Channels[result.Index]))
}
continue
}
if result.Index < 0 || result.Index >= len(s.Channels) {
continue
}
ch := &s.Channels[result.Index]
ch.ChannelID = result.Channel.ID
if result.Channel.Username != nil {
ch.Username = *result.Channel.Username
}
if result.Channel.Title != nil {
ch.Title = *result.Channel.Title
}
}
return failed
}
func (s *SelectChannelsForPurchase) Handle(b *bot.Bot, u *echotron.Update) {
@@ -380,3 +405,52 @@ func (s *SelectChannelsForPurchase) Handle(b *bot.Bot, u *echotron.Update) {
}
func (s *SelectChannelsForPurchase) Exit() {}
func channelLabel(ch PurchaseChannelInput) string {
if ch.Username != "" {
return "@" + ch.Username
}
if ch.Title != "" {
return ch.Title
}
if ch.InviteLink != "" {
return "invite link"
}
if ch.ChannelID != "" {
return ch.ChannelID
}
return "канал"
}
func channelKey(ch PurchaseChannelInput) string {
if ch.ChannelID != "" {
return ch.ChannelID
}
if ch.Username != "" {
return ch.Username
}
if ch.InviteLink != "" {
return ch.InviteLink
}
return ""
}
func channelLabelByKey(channels []PurchaseChannelInput, key string) string {
for _, ch := range channels {
if channelKey(ch) == key {
return channelLabel(ch)
}
}
return key
}
func isInviteLink(value string) bool {
value = strings.TrimSpace(value)
if strings.Contains(value, "t.me/") || strings.Contains(value, "telegram.me/") {
return true
}
if strings.HasPrefix(value, "tg://") || strings.HasPrefix(value, "tg:") {
return true
}
return false
}