размещения ручки
This commit is contained in:
@@ -555,27 +555,36 @@ type PlacementDetails struct {
|
||||
PlacementType *string `json:"placement_type,omitempty"`
|
||||
Format *string `json:"format,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
CreativeID *string `json:"creative_id,omitempty"`
|
||||
}
|
||||
|
||||
type PlacementOutput struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
InviteLink string `json:"invite_link"`
|
||||
InviteLinkType string `json:"invite_link_type"`
|
||||
Channel Channel `json:"channel"`
|
||||
Details *PlacementDetails `json:"details,omitempty"`
|
||||
PlacementPosts []PlacementPostOutput `json:"placement_posts,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
CreativeID *string `json:"creative_id,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
InviteLink *string `json:"invite_link,omitempty"`
|
||||
InviteLinkType string `json:"invite_link_type"`
|
||||
Channel Channel `json:"channel"`
|
||||
Details *PlacementDetails `json:"details,omitempty"`
|
||||
PlacementPost *PlacementPostOutput `json:"placement_post,omitempty"`
|
||||
}
|
||||
|
||||
type PlacementPostOutput struct {
|
||||
ID string `json:"id"`
|
||||
PlacementID string `json:"placement_id"`
|
||||
Placement PlacementOutput `json:"placement"`
|
||||
AdPostURL *string `json:"ad_post_url,omitempty"`
|
||||
Status string `json:"status"`
|
||||
SubscriptionsCount int `json:"subscriptions_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
SubscriptionsCount int `json:"subscriptions_count"`
|
||||
ViewsCount *int `json:"views_count,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Post PostOutput `json:"post"`
|
||||
}
|
||||
|
||||
type PostOutput struct {
|
||||
ID string `json:"id"`
|
||||
MessageID int `json:"message_id"`
|
||||
Text string `json:"text"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
DeletedFromChannelAt *string `json:"deleted_from_channel_at,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreatePlacementChannelInput struct {
|
||||
@@ -586,7 +595,7 @@ type CreatePlacementChannelInput struct {
|
||||
}
|
||||
|
||||
type CreatePlacementsInput struct {
|
||||
CreativeID string `json:"creative_id"`
|
||||
CreativeID *string `json:"creative_id,omitempty"`
|
||||
Channels []CreatePlacementChannelInput `json:"channels"`
|
||||
Details *PlacementDetails `json:"details,omitempty"`
|
||||
}
|
||||
@@ -595,6 +604,15 @@ type GetPlacementsOutput struct {
|
||||
Placements []PlacementOutput `json:"placements"`
|
||||
}
|
||||
|
||||
type CreativePreviewOutput struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Text string `json:"text"`
|
||||
MediaType string `json:"media_type"`
|
||||
MediaFileID string `json:"media_file_id"`
|
||||
Buttons []CreativeButton `json:"buttons"`
|
||||
}
|
||||
|
||||
func (c *Client) CreatePlacements(
|
||||
ctx context.Context,
|
||||
jwt string,
|
||||
@@ -661,6 +679,33 @@ func (c *Client) GetPlacement(
|
||||
return &placement, err
|
||||
}
|
||||
|
||||
func (c *Client) BuildPlacementCreative(
|
||||
ctx context.Context,
|
||||
jwt string,
|
||||
workspaceID string,
|
||||
projectID string,
|
||||
placementID string,
|
||||
) (*CreativePreviewOutput, error) {
|
||||
path := fmt.Sprintf(
|
||||
"api/v1/workspaces/%s/projects/%s/placements/%s/creative",
|
||||
workspaceID,
|
||||
projectID,
|
||||
placementID,
|
||||
)
|
||||
|
||||
var resp CreativePreviewOutput
|
||||
err := c.do(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
path,
|
||||
nil,
|
||||
&resp,
|
||||
withBearer(jwt),
|
||||
)
|
||||
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Workspace Members
|
||||
// ============================================================================
|
||||
|
||||
@@ -329,13 +329,16 @@ func (b *Bot) Update(u *echotron.Update) {
|
||||
|
||||
// 1. Команды (приоритетный маршрут)
|
||||
if u.Message != nil {
|
||||
switch u.Message.Text {
|
||||
case "/start":
|
||||
switch {
|
||||
case u.Message.Text == "/start":
|
||||
b.SetState(b.commandRouter("/start"), NewMessage)
|
||||
return
|
||||
case "/start login":
|
||||
case u.Message.Text == "/start login":
|
||||
b.SetState(b.commandRouter("/start login"), NewMessage)
|
||||
return
|
||||
case strings.HasPrefix(u.Message.Text, "/start project_"):
|
||||
b.SetState(b.commandRouter(u.Message.Text), NewMessage)
|
||||
return
|
||||
}
|
||||
|
||||
b.CurrentState.HandleMessage(b, u)
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -24,11 +25,19 @@ func main() {
|
||||
})
|
||||
|
||||
var commandRouter = func(command string) bot.State {
|
||||
switch command {
|
||||
case "/start":
|
||||
createCreativeRe := regexp.MustCompile(`^/start project_(.+)_createcreative$`)
|
||||
|
||||
switch {
|
||||
case command == "/start":
|
||||
return &screens.MainMenu{}
|
||||
case "/start login":
|
||||
case command == "/start login":
|
||||
return &screens.Login{}
|
||||
case createCreativeRe.MatchString(command):
|
||||
projectID := createCreativeRe.FindStringSubmatch(command)[1]
|
||||
return &screens.AddCreativeStart{Ctx: &screens.AddCreativeCtx{
|
||||
ProjectID: projectID,
|
||||
BackState: &screens.MainMenu{},
|
||||
}}
|
||||
}
|
||||
panic("Unknown command: " + command)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ type AddCreativeCtx struct {
|
||||
BackState bot.State
|
||||
}
|
||||
|
||||
type AddCreativeStart struct{ ctx *AddCreativeCtx }
|
||||
type AddCreativeStart struct{ Ctx *AddCreativeCtx }
|
||||
|
||||
type AddCreativeEdit struct{ ctx *AddCreativeCtx }
|
||||
|
||||
@@ -102,8 +102,8 @@ func (s *AddCreativeStart) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
|
||||
return
|
||||
}
|
||||
if u.CallbackQuery.Data == "cancel" && s.ctx.BackState != nil {
|
||||
b.SetState(s.ctx.BackState, bot.EditMessage)
|
||||
if u.CallbackQuery.Data == "cancel" && s.Ctx.BackState != nil {
|
||||
b.SetState(s.Ctx.BackState, bot.EditMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,14 +112,14 @@ func (s *AddCreativeStart) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
||||
return
|
||||
}
|
||||
|
||||
s.ctx.extractCreativeFromMessage(u.Message)
|
||||
if s.ctx.Name == nil {
|
||||
name := s.ctx.generateCreativeName()
|
||||
s.ctx.Name = &name
|
||||
s.Ctx.extractCreativeFromMessage(u.Message)
|
||||
if s.Ctx.Name == nil {
|
||||
name := s.Ctx.generateCreativeName()
|
||||
s.Ctx.Name = &name
|
||||
}
|
||||
|
||||
s.ctx.DeleteUserMessage(b, u.Message.ID)
|
||||
b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.NewMessage)
|
||||
s.Ctx.DeleteUserMessage(b, u.Message.ID)
|
||||
b.SetState(&AddCreativeEdit{ctx: s.Ctx}, bot.NewMessage)
|
||||
}
|
||||
|
||||
func (s *AddCreativeStart) Handle(_ *bot.Bot, _ *echotron.Update) {}
|
||||
|
||||
@@ -109,7 +109,7 @@ func (s *Creatives) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
b.Edit("📦 Архив креативов\n\nЭта функция в разработке", Keyboard(Row(Button("← Назад", "back"))))
|
||||
|
||||
case data == "add_creative":
|
||||
b.SetState(&AddCreativeStart{ctx: &AddCreativeCtx{
|
||||
b.SetState(&AddCreativeStart{Ctx: &AddCreativeCtx{
|
||||
ProjectID: s.ProjectID,
|
||||
BackState: s,
|
||||
}}, bot.EditMessage)
|
||||
|
||||
@@ -28,10 +28,7 @@ func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode) {
|
||||
return
|
||||
}
|
||||
|
||||
var placementPost *backend.PlacementPostOutput
|
||||
if len(placement.PlacementPosts) > 0 {
|
||||
placementPost = &placement.PlacementPosts[0]
|
||||
}
|
||||
placementPost := placement.PlacementPost
|
||||
|
||||
text := "<b>Детали размещения</b>\n\n"
|
||||
|
||||
@@ -69,12 +66,11 @@ func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode) {
|
||||
|
||||
if placementPost != nil {
|
||||
text += "\n<b>Пост</b>\n"
|
||||
if placementPost.AdPostURL != nil && *placementPost.AdPostURL != "" {
|
||||
text += fmt.Sprintf("• <b>Ссылка на пост:</b> %s\n", *placementPost.AdPostURL)
|
||||
if placementPost.Post.URL != nil && *placementPost.Post.URL != "" {
|
||||
text += fmt.Sprintf("• <b>Ссылка на пост:</b> %s\n", *placementPost.Post.URL)
|
||||
} else {
|
||||
text += "• <b>Ссылка на пост:</b> не найдена\n"
|
||||
}
|
||||
text += fmt.Sprintf("• <b>Статус поста:</b> %s\n", formatPlacementPostStatus(placementPost.Status))
|
||||
}
|
||||
|
||||
keyboard := Keyboard(Row(Button("← Назад", "back")))
|
||||
@@ -128,18 +124,6 @@ func formatPlacementType(value string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func formatPlacementPostStatus(status string) string {
|
||||
normalized := strings.TrimSpace(strings.ToLower(status))
|
||||
switch normalized {
|
||||
case "active":
|
||||
return "Активен"
|
||||
case "archived":
|
||||
return "Архивирован"
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
func formatCostType(value string) string {
|
||||
switch strings.TrimSpace(strings.ToLower(value)) {
|
||||
case "cpm":
|
||||
|
||||
@@ -3,7 +3,6 @@ package screens
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -1425,7 +1424,7 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
|
||||
}
|
||||
|
||||
input := backend.CreatePlacementsInput{
|
||||
CreativeID: s.CreativeID,
|
||||
CreativeID: &s.CreativeID,
|
||||
Channels: apiChannels,
|
||||
Details: placementDetails,
|
||||
}
|
||||
@@ -1439,20 +1438,24 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
|
||||
return
|
||||
}
|
||||
|
||||
creative, err := b.Backend.GetCreative(context.Background(), jwt, b.Session.WorkspaceID, s.CreativeID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get creative")
|
||||
creative = nil
|
||||
}
|
||||
|
||||
if creative != nil {
|
||||
for _, placement := range placements.Placements {
|
||||
channelName := s.formatChannelName(placement)
|
||||
header := fmt.Sprintf("<b>Канал:</b> %s", channelName)
|
||||
b.SendMessage(header, b.ChatID, &echotron.MessageOptions{ParseMode: echotron.HTML})
|
||||
|
||||
s.sendCreativeWithInviteLink(b, creative, placement.InviteLink)
|
||||
for _, placement := range placements.Placements {
|
||||
creativePreview, err := b.Backend.BuildPlacementCreative(
|
||||
context.Background(),
|
||||
jwt,
|
||||
b.Session.WorkspaceID,
|
||||
s.ProjectID,
|
||||
placement.ID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to build placement creative")
|
||||
continue
|
||||
}
|
||||
|
||||
channelName := s.formatChannelName(placement)
|
||||
header := fmt.Sprintf("<b>Канал:</b> %s", channelName)
|
||||
b.SendMessage(header, b.ChatID, &echotron.MessageOptions{ParseMode: echotron.HTML})
|
||||
|
||||
s.sendCreativePreview(b, creativePreview)
|
||||
}
|
||||
|
||||
b.SetState(&Placements{
|
||||
@@ -1588,15 +1591,12 @@ func (s *PurchaseOptionalDetails) formatChannelName(placement backend.PlacementO
|
||||
return "Без названия"
|
||||
}
|
||||
|
||||
func (s *PurchaseOptionalDetails) sendCreativeWithInviteLink(
|
||||
func (s *PurchaseOptionalDetails) sendCreativePreview(
|
||||
b *bot.Bot,
|
||||
creative *backend.Creative,
|
||||
inviteLink string,
|
||||
creative *backend.CreativePreviewOutput,
|
||||
) {
|
||||
log.Info().Str("invite_link", inviteLink).Msg("Sending creative with invite link")
|
||||
linkedText := s.injectInviteLink(creative.Text, inviteLink)
|
||||
editor := CreativeEditorFields{
|
||||
Text: &linkedText,
|
||||
Text: &creative.Text,
|
||||
MediaType: creative.MediaType,
|
||||
MediaFileID: creative.MediaFileID,
|
||||
}
|
||||
@@ -1604,34 +1604,12 @@ func (s *PurchaseOptionalDetails) sendCreativeWithInviteLink(
|
||||
if len(creative.Buttons) > 0 {
|
||||
editor.Buttons = make([]InlineButton, 0, len(creative.Buttons))
|
||||
for _, btn := range creative.Buttons {
|
||||
url := btn.URL
|
||||
if url == inviteLinkPlaceholder {
|
||||
url = inviteLink
|
||||
}
|
||||
editor.Buttons = append(editor.Buttons, InlineButton{
|
||||
Text: btn.Text,
|
||||
URL: url,
|
||||
URL: btn.URL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
editor.SendCreativePreview(b)
|
||||
}
|
||||
|
||||
func (s *PurchaseOptionalDetails) injectInviteLink(text, inviteLink string) string {
|
||||
if text == "" {
|
||||
return text
|
||||
}
|
||||
re := regexp.MustCompile(`<a\s+class="tg-link">([^<]*)</a>`)
|
||||
return re.ReplaceAllStringFunc(text, func(match string) string {
|
||||
sub := re.FindStringSubmatch(match)
|
||||
if len(sub) < 2 {
|
||||
return match
|
||||
}
|
||||
inner := strings.TrimSpace(sub[1])
|
||||
if inner == "" {
|
||||
return fmt.Sprintf(`<a href="%s">%s</a>`, inviteLink, inviteLink)
|
||||
}
|
||||
return fmt.Sprintf(`<a href="%s">%s</a>`, inviteLink, sub[1])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
|
||||
}
|
||||
|
||||
input := backend.CreatePlacementsInput{
|
||||
CreativeID: s.CreativeID,
|
||||
CreativeID: &s.CreativeID,
|
||||
Channels: apiChannels,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user