правки

This commit is contained in:
Artem Tsyrulnikov
2026-02-01 18:13:46 +03:00
parent 4e35c73a66
commit 52cd2ec1a0
8 changed files with 503 additions and 115 deletions

View File

@@ -397,6 +397,17 @@ class Postgres(DatabaseBase):
'project', 'project__channel', 'channel', 'creative' 'project', 'project__channel', 'channel', 'creative'
) )
@staticmethod
async def count_placements_by_project_and_channel(
project_id: uuid.UUID,
channel_id: uuid.UUID,
) -> int:
"""Count placements for a project in a specific channel."""
return await domain.Placement.filter(
project_id=project_id,
channel_id=channel_id,
).count()
@staticmethod @staticmethod
async def update_placement(placement: domain.Placement) -> None: async def update_placement(placement: domain.Placement) -> None:
await placement.save() await placement.save()

View File

@@ -9,6 +9,12 @@ from src.domain.placement_post import PlacementPostStatus
from .channel import ChannelOutput from .channel import ChannelOutput
class ProjectOutput(pydantic.BaseModel):
id: uuid.UUID
status: str
channel: ChannelOutput
class CostInfo(pydantic.BaseModel): class CostInfo(pydantic.BaseModel):
type: CostType type: CostType
value: float value: float
@@ -30,11 +36,14 @@ class PlacementOutput(pydantic.BaseModel):
id: uuid.UUID id: uuid.UUID
status: PlacementStatus status: PlacementStatus
creative_id: uuid.UUID | None = None creative_id: uuid.UUID | None = None
creative_name: str | None = None
comment: str | None = None comment: str | None = None
invite_link: str | None invite_link: str | None
invite_link_created_at: datetime.datetime | None = None invite_link_created_at: datetime.datetime | None = None
invite_link_type: InviteLinkType invite_link_type: InviteLinkType
channel: ChannelOutput channel: ChannelOutput
project: ProjectOutput | None = None
placement_number: int
details: PlacementDetails | None = None details: PlacementDetails | None = None

View File

@@ -31,16 +31,41 @@ def _build_placement_details(placement: domain.Placement) -> dto.PlacementDetail
return None return None
def _build_placement_output(placement: domain.Placement) -> dto.PlacementOutput: def _build_placement_output(
placement: domain.Placement,
project: domain.Project | None = None,
placement_number: int = 0,
creative_name: str | None = None,
) -> dto.PlacementOutput:
channel = placement.channel channel = placement.channel
if channel is None: if channel is None:
log.error('Placement %s has no channel prefetched', placement.id) log.error('Placement %s has no channel prefetched', placement.id)
raise ValueError(f'Placement {placement.id} has no channel') raise ValueError(f'Placement {placement.id} has no channel')
# Build project output if project is provided
project_output: dto.ProjectOutput | None = None
if project is not None:
project_channel = project.channel
if project_channel is None:
log.error('Project %s has no channel', project.id)
raise ValueError(f'Project {project.id} has no channel')
project_output = dto.ProjectOutput(
id=project.id,
status=project.status.value,
channel=dto.ChannelOutput(
id=project_channel.id,
telegram_id=project_channel.telegram_id,
title=project_channel.title,
username=project_channel.username,
),
)
return dto.PlacementOutput( return dto.PlacementOutput(
id=placement.id, id=placement.id,
status=placement.status, status=placement.status,
creative_id=placement.creative_id, creative_id=placement.creative_id,
creative_name=creative_name,
comment=placement.comment, comment=placement.comment,
invite_link=placement.invite_link, invite_link=placement.invite_link,
invite_link_type=placement.invite_link_type, invite_link_type=placement.invite_link_type,
@@ -50,6 +75,8 @@ def _build_placement_output(placement: domain.Placement) -> dto.PlacementOutput:
title=channel.title, title=channel.title,
username=channel.username, username=channel.username,
), ),
project=project_output,
placement_number=placement_number,
details=_build_placement_details(placement), details=_build_placement_details(placement),
) )
@@ -73,7 +100,6 @@ async def create_placements(
if project.channel.telegram_id is None: if project.channel.telegram_id is None:
raise domain.ChannelNotFound(project.channel.id) raise domain.ChannelNotFound(project.channel.id)
invite_link_type = project.purchase_invite_type_default
details = input.details details = input.details
placements: list[domain.Placement] = [] placements: list[domain.Placement] = []
creatives_by_id: dict[uuid.UUID, domain.Creative] = {} creatives_by_id: dict[uuid.UUID, domain.Creative] = {}
@@ -106,7 +132,9 @@ async def create_placements(
creative_id=creative.id if creative else None, creative_id=creative.id if creative else None,
channel_id=channel.id, channel_id=channel.id,
invite_link=None, invite_link=None,
invite_link_type=invite_link_type, invite_link_type=(channel_details.invite_link_type if channel_details and channel_details.invite_link_type else None)
or (details.invite_link_type if details and details.invite_link_type else None)
or project.purchase_invite_type_default,
status=channel_input.status or domain.PlacementStatus.NO_STATUS, status=channel_input.status or domain.PlacementStatus.NO_STATUS,
comment=channel_input.comment, comment=channel_input.comment,
# Приоритет: channel details > общие details # Приоритет: channel details > общие details
@@ -138,7 +166,7 @@ async def create_placements(
placement_outputs = [] placement_outputs = []
for placement in placements: for placement in placements:
placement_output = _build_placement_output(placement) placement_output = _build_placement_output(placement, project=project)
placement_outputs.append( placement_outputs.append(
dto.PlacementWithPostsOutput( dto.PlacementWithPostsOutput(
**placement_output.model_dump(), **placement_output.model_dump(),

View File

@@ -77,6 +77,30 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d
channel = await self.database.get_channel(channel_id=placement.channel_id) channel = await self.database.get_channel(channel_id=placement.channel_id)
placement.channel = channel placement.channel = channel
# Prefetch project channel for output building
if project.channel is None:
project_channel = await self.database.get_channel(channel_id=project.channel_id)
project.channel = project_channel
# Get creative name if exists
creative_name = None
if placement.creative_id:
creative = await self.database.get_creative(input.workspace_id, placement.creative_id)
if creative:
creative_name = creative.name
# Calculate placement number (ordinal number of this placement)
placement_number = await self.database.count_placements_by_project_and_channel(
project.id, placement.channel_id
)
# Count placements created before this one
placements_before = await domain.Placement.filter(
project_id=project.id,
channel_id=placement.channel_id,
created_at__lt=placement.created_at,
).count()
placement_number = placements_before + 1
placement_posts = await self.database.get_workspace_placement_posts( placement_posts = await self.database.get_workspace_placement_posts(
input.workspace_id, input.workspace_id,
placement_id=placement.id, placement_id=placement.id,
@@ -112,7 +136,12 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d
if placement_post_output is not None: if placement_post_output is not None:
break break
placement_output = _build_placement_output(placement) placement_output = _build_placement_output(
placement,
project=project,
placement_number=placement_number,
creative_name=creative_name,
)
return dto.PlacementWithPostsOutput( return dto.PlacementWithPostsOutput(
**placement_output.model_dump(), **placement_output.model_dump(),
placement_post=placement_post_output, placement_post=placement_post_output,

View File

@@ -82,7 +82,7 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
placement_outputs = [] placement_outputs = []
for placement in placements: for placement in placements:
placement_output = _build_placement_output(placement) placement_output = _build_placement_output(placement, project=project)
placement_post_output = None placement_post_output = None
placement_posts_for_placement = placement_posts_by_placement_id.get(placement.id, []) placement_posts_for_placement = placement_posts_by_placement_id.get(placement.id, [])
if placement_posts_for_placement: if placement_posts_for_placement:

View File

@@ -550,6 +550,7 @@ type Channel struct {
TelegramID *int64 `json:"telegram_id"` TelegramID *int64 `json:"telegram_id"`
Title *string `json:"title"` Title *string `json:"title"`
Username *string `json:"username"` Username *string `json:"username"`
InviteLink *string `json:"invite_link"`
} }
type CreateChannelInput struct { type CreateChannelInput struct {
@@ -568,6 +569,12 @@ type CreateChannelResult struct {
Error *string `json:"error,omitempty"` Error *string `json:"error,omitempty"`
} }
type ProjectOutput struct {
ID string `json:"id"`
Status string `json:"status"`
Channel Channel `json:"channel"`
}
type CreateChannelsOutput struct { type CreateChannelsOutput struct {
Results []CreateChannelResult `json:"results"` Results []CreateChannelResult `json:"results"`
} }
@@ -586,24 +593,30 @@ type PlacementDetails struct {
Format *string `json:"format,omitempty"` Format *string `json:"format,omitempty"`
Comment *string `json:"comment,omitempty"` Comment *string `json:"comment,omitempty"`
CreativeID *string `json:"creative_id,omitempty"` CreativeID *string `json:"creative_id,omitempty"`
CreativeName *string `json:"creative_name,omitempty"`
InviteLinkType *string `json:"invite_link_type,omitempty"`
} }
type PlacementOutput struct { type PlacementOutput struct {
ID string `json:"id"` ID string `json:"id"`
Status string `json:"status"` Status string `json:"status"`
CreativeID *string `json:"creative_id,omitempty"` CreativeID *string `json:"creative_id,omitempty"`
Comment *string `json:"comment,omitempty"` CreativeName *string `json:"creative_name,omitempty"`
InviteLink *string `json:"invite_link,omitempty"` Comment *string `json:"comment,omitempty"`
InviteLinkType string `json:"invite_link_type"` InviteLink *string `json:"invite_link,omitempty"`
Channel Channel `json:"channel"` InviteLinkType string `json:"invite_link_type"`
Details *PlacementDetails `json:"details,omitempty"` Channel Channel `json:"channel"`
PlacementPost *PlacementPostOutput `json:"placement_post,omitempty"` Project *ProjectOutput `json:"project"`
PlacementNumber int `json:"placement_number"`
Details *PlacementDetails `json:"details,omitempty"`
PlacementPost *PlacementPostOutput `json:"placement_post,omitempty"`
} }
type PlacementPostOutput struct { type PlacementPostOutput struct {
SubscriptionsCount int `json:"subscriptions_count"` SubscriptionsCount int `json:"subscriptions_count"`
ViewsCount *int `json:"views_count,omitempty"` ViewsCount *int `json:"views_count,omitempty"`
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
TimeOnTop *int `json:"time_on_top,omitempty"`
Post PostOutput `json:"post"` Post PostOutput `json:"post"`
} }

View File

@@ -32,62 +32,164 @@ func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode) {
text := "<b>Детали размещения</b>\n\n" text := "<b>Детали размещения</b>\n\n"
channelName := formatChannelName(placement.Channel) // Размещение в канале
text += fmt.Sprintf(" <b>Канал:</b> %s\n", channelName) text += "📺 <b>Размещение в канале:</b>\n"
text += fmt.Sprintf("• <b>Статус:</b> %s\n", formatPlacementStatus(placement.Status)) text += formatChannelWithName(placement.Channel)
text += fmt.Sprintf("• <b>Ссылка:</b> %s\n", formatInviteLinkType(placement.InviteLinkType)) text += "\n"
// Рекламируемый проект
if placement.Project != nil {
text += "🎯 <b>Рекламируемый проект:</b>\n"
text += formatProject(*placement.Project)
text += "\n"
}
// Ссылка на пост
if placementPost != nil && placementPost.Post.URL != nil && *placementPost.Post.URL != "" {
text += fmt.Sprintf("🔗 <b>Ссылка на пост:</b> %s\n", *placementPost.Post.URL)
}
// Пригласительная ссылка
if placement.InviteLink != nil && *placement.InviteLink != "" {
text += fmt.Sprintf("📩 <b>Пригласительная ссылка:</b> %s\n", *placement.InviteLink)
}
// Тип ссылки
text += fmt.Sprintf("🔓 <b>Тип ссылки:</b> %s\n", formatInviteLinkType(placement.InviteLinkType))
// Порядковый номер размещения
if placement.PlacementNumber > 0 {
text += fmt.Sprintf("🔢 <b>Размещение №%d</b> в этом канале\n", placement.PlacementNumber)
}
// Креатив
if placement.Details != nil && placement.Details.CreativeName != nil && *placement.Details.CreativeName != "" {
text += fmt.Sprintf("✨ <b>Креатив:</b> %s\n", *placement.Details.CreativeName)
} else if placement.CreativeName != nil && *placement.CreativeName != "" {
text += fmt.Sprintf("✨ <b>Креатив:</b> %s\n", *placement.CreativeName)
}
text += "\n"
// Финансовая информация
if placement.Details != nil { if placement.Details != nil {
if placement.Details.PlacementType != nil { // Стоимость
text += fmt.Sprintf("• <b>Тип:</b> %s\n", formatPlacementType(*placement.Details.PlacementType))
}
if placement.Details.PlacementAt != nil && *placement.Details.PlacementAt != "" {
text += fmt.Sprintf("• <b>Дата размещения:</b> %s\n", formatDateTime(*placement.Details.PlacementAt))
}
if placement.Details.PaymentAt != nil && *placement.Details.PaymentAt != "" {
text += fmt.Sprintf("• <b>Дата оплаты:</b> %s\n", formatDateTime(*placement.Details.PaymentAt))
}
if placement.Details.Cost != nil { if placement.Details.Cost != nil {
text += fmt.Sprintf("• <b>Стоимость:</b> %s %.0f₽\n", costType := formatCostType(placement.Details.Cost.Type)
formatCostType(placement.Details.Cost.Type), text += fmt.Sprintf("💰 <b>Стоимость:</b> %s %.0f₽\n", costType, placement.Details.Cost.Value)
placement.Details.Cost.Value,
)
} }
// Стоимость до торга
if placement.Details.CostBeforeBargain != nil { if placement.Details.CostBeforeBargain != nil {
text += fmt.Sprintf("• <b>До торга:</b> %s %.0f₽\n", costType := formatCostType(placement.Details.CostBeforeBargain.Type)
formatCostType(placement.Details.CostBeforeBargain.Type), text += fmt.Sprintf("💸 <b>До торга:</b> %s %.0f₽\n", costType, placement.Details.CostBeforeBargain.Value)
placement.Details.CostBeforeBargain.Value,
)
} }
// Дата размещения
if placement.Details.PlacementAt != nil && *placement.Details.PlacementAt != "" {
text += fmt.Sprintf("📅 <b>Дата размещения:</b> %s\n", formatDateTime(*placement.Details.PlacementAt))
}
// Дата оплаты
if placement.Details.PaymentAt != nil && *placement.Details.PaymentAt != "" {
text += fmt.Sprintf("💳 <b>Дата оплаты:</b> %s\n", formatDateTime(*placement.Details.PaymentAt))
}
// Формат
if placement.Details.Format != nil && *placement.Details.Format != "" { if placement.Details.Format != nil && *placement.Details.Format != "" {
text += fmt.Sprintf(" <b>Формат:</b> %s\n", *placement.Details.Format) text += fmt.Sprintf("📐 <b>Формат:</b> %s\n", *placement.Details.Format)
} }
if placement.Details.Comment != nil && *placement.Details.Comment != "" {
text += fmt.Sprintf("• <b>Комментарий:</b> %s\n", *placement.Details.Comment) // Тип закупа
if placement.Details.PlacementType != nil {
text += fmt.Sprintf("🔄 <b>Тип закупа:</b> %s\n", formatPlacementType(*placement.Details.PlacementType))
} }
} }
text += "\n"
// Статистика
if placementPost != nil { if placementPost != nil {
text += "\n<b>Пост</b>\n" // Кол-во подписчиков
if placementPost.Post.URL != nil && *placementPost.Post.URL != "" { if placementPost.SubscriptionsCount > 0 {
text += fmt.Sprintf(" <b>Ссылка на пост:</b> %s\n", *placementPost.Post.URL) text += fmt.Sprintf("👥 <b>Подписчики:</b> %d\n", placementPost.SubscriptionsCount)
} else {
text += "• <b>Ссылка на пост:</b> не найдена\n"
} }
// Просмотры поста
if placementPost.ViewsCount != nil {
text += fmt.Sprintf("👁️ <b>Просмотры:</b> %d\n", *placementPost.ViewsCount)
}
// CPF (стоимость подписчика)
if placementPost.SubscriptionsCount > 0 && placement.Details != nil && placement.Details.Cost != nil {
cpf := calculateCPF(placement.Details.Cost.Value, placementPost.SubscriptionsCount)
text += fmt.Sprintf("💵 <b>CPF:</b> %.2f₽\n", cpf)
}
// Конверсия
if placementPost.ViewsCount != nil && *placementPost.ViewsCount > 0 && placementPost.SubscriptionsCount > 0 {
conversion := calculateConversion(placementPost.SubscriptionsCount, *placementPost.ViewsCount)
text += fmt.Sprintf("📊 <b>Конверсия:</b> %.1f%%\n", conversion)
}
}
// Комментарий
if placement.Details != nil && placement.Details.Comment != nil && *placement.Details.Comment != "" {
text += fmt.Sprintf("\n📝 <b>Комментарий:</b> %s\n", *placement.Details.Comment)
} }
keyboard := Keyboard(Row(Button("← Назад", "back"))) keyboard := Keyboard(Row(Button("← Назад", "back")))
b.Render(text, keyboard, mode) b.Render(text, keyboard, mode)
} }
func formatChannelName(channel backend.Channel) string { func formatChannelWithName(channel backend.Channel) string {
var name string
if channel.Title != nil && *channel.Title != "" { if channel.Title != nil && *channel.Title != "" {
return *channel.Title name = *channel.Title
} else if channel.Username != nil && *channel.Username != "" {
name = "@" + *channel.Username
} else {
name = "Без названия"
} }
// Добавляем ссылку если есть username или invite_link
if channel.Username != nil && *channel.Username != "" { if channel.Username != nil && *channel.Username != "" {
return "@" + *channel.Username return fmt.Sprintf(" %s (@%s)\n", name, *channel.Username)
} else if channel.InviteLink != nil && *channel.InviteLink != "" {
return fmt.Sprintf(" %s\n", name)
} }
return "Без названия" return fmt.Sprintf(" %s\n", name)
}
func formatProject(project backend.ProjectOutput) string {
var name string
if project.Channel.Title != nil && *project.Channel.Title != "" {
name = *project.Channel.Title
} else if project.Channel.Username != nil && *project.Channel.Username != "" {
name = "@" + *project.Channel.Username
} else {
name = "Без названия"
}
// Добавляем ссылку если есть username
if project.Channel.Username != nil && *project.Channel.Username != "" {
return fmt.Sprintf(" %s (@%s)\n", name, *project.Channel.Username)
}
return fmt.Sprintf(" %s\n", name)
}
func calculateCPF(cost float64, subscriptions int) float64 {
if subscriptions == 0 {
return 0
}
return cost / float64(subscriptions)
}
func calculateConversion(subscriptions int, views int) float64 {
if views == 0 {
return 0
}
return float64(subscriptions) / float64(views) * 100
} }
func formatInviteLinkType(value string) string { func formatInviteLinkType(value string) string {

View File

@@ -15,48 +15,52 @@ import (
) )
type PurchaseOptionalDetails struct { type PurchaseOptionalDetails struct {
ProjectID string ProjectID string
ProjectTitle string ProjectTitle string
CreativeID string CreativeID string
CreativeTitle string CreativeTitle string
Channels []PurchaseChannelInput Channels []PurchaseChannelInput
PlacementDateTime *time.Time PlacementDateTime *time.Time
PaymentDate *time.Time PaymentDate *time.Time
CostType string CostType string
CostValue *float64 CostValue *float64
CostBeforeType string CostBeforeType string
CostBeforeBargain *CostEntry CostBeforeBargain *CostEntry
PurchaseType string PurchaseType string
Format string Format string
Comment string Comment string
InputMode string InviteLinkType string
CurrentParam string InputMode string
CurrentChannel string CurrentParam string
ParamPage int CurrentChannel string
ReturnMode string ParamPage int
PlacementMode string ReturnMode string
PaymentDateMode string PlacementMode string
CostMode string PaymentDateMode string
CostBeforeMode string CostMode string
PurchaseTypeMode string CostBeforeMode string
CommentMode string PurchaseTypeMode string
FormatMode string CommentMode string
PlacementByChannel map[string]*time.Time FormatMode string
PaymentDateByChannel map[string]*time.Time InviteLinkTypeMode string
CostByChannel map[string]CostEntry PlacementByChannel map[string]*time.Time
CostBeforeByChannel map[string]CostEntry PaymentDateByChannel map[string]*time.Time
PurchaseTypeByChannel map[string]string CostByChannel map[string]CostEntry
CommentByChannel map[string]string CostBeforeByChannel map[string]CostEntry
FormatByChannel map[string]string PurchaseTypeByChannel map[string]string
PlacementCopy *time.Time CommentByChannel map[string]string
PaymentDateCopy *time.Time FormatByChannel map[string]string
CostCopy *CostEntry InviteLinkTypeByChannel map[string]string
CostBeforeCopy *CostEntry PlacementCopy *time.Time
PurchaseTypeCopy *string PaymentDateCopy *time.Time
CommentCopy *string CostCopy *CostEntry
FormatCopy *string CostBeforeCopy *CostEntry
ChannelEditMode string // "edit" или "copy" PurchaseTypeCopy *string
BackState bot.State CommentCopy *string
FormatCopy *string
InviteLinkTypeCopy *string
ChannelEditMode string // "edit" или "copy"
BackState bot.State
} }
type CostEntry struct { type CostEntry struct {
@@ -104,6 +108,7 @@ func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
)) ))
rows = append(rows, Row( rows = append(rows, Row(
s.formatButton(), s.formatButton(),
s.inviteLinkTypeButton(),
)) ))
if row := s.costBeforeDeleteRow(); row != nil { if row := s.costBeforeDeleteRow(); row != nil {
@@ -182,6 +187,16 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.openCommonEditor(b, "format") s.openCommonEditor(b, "format")
} }
case "opt_invite_link_type":
// Открываем редактор в текущем режиме
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
s.InputMode = "invite_link_type_channels"
s.Enter(b, bot.EditMessage)
} else {
s.InputMode = "invite_link_type"
s.Enter(b, bot.EditMessage)
}
case "opt_comment": case "opt_comment":
// Открываем редактор в текущем режиме // Открываем редактор в текущем режиме
if s.CommentMode == "per_channel" && len(s.Channels) > 1 { if s.CommentMode == "per_channel" && len(s.Channels) > 1 {
@@ -332,6 +347,19 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
return return
} }
if strings.HasPrefix(u.CallbackQuery.Data, "invite_link_type:") {
value := strings.TrimPrefix(u.CallbackQuery.Data, "invite_link_type:")
s.setInviteLinkTypeValue(value)
if s.ReturnMode != "" {
s.InputMode = s.ReturnMode
s.ReturnMode = ""
} else {
s.InputMode = ""
}
s.CurrentChannel = ""
s.Enter(b, bot.EditMessage)
return
}
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
} }
} }
@@ -433,6 +461,9 @@ func (s *PurchaseOptionalDetails) formatOptionalSummary() string {
if s.hasFormatValue() { if s.hasFormatValue() {
lines = append(lines, fmt.Sprintf("<i>Формат:</i> <b>%s</b>%s", s.formatFormatSummary(), s.formatFormatDetails())) lines = append(lines, fmt.Sprintf("<i>Формат:</i> <b>%s</b>%s", s.formatFormatSummary(), s.formatFormatDetails()))
} }
if s.hasInviteLinkTypeValue() {
lines = append(lines, fmt.Sprintf("<i>Тип ссылки:</i> <b>%s</b>%s", s.formatInviteLinkTypeSummary(), s.formatInviteLinkTypeDetails()))
}
if s.hasCommentValue() { if s.hasCommentValue() {
lines = append(lines, fmt.Sprintf("<i>Комментарий:</i> <b>%s</b>%s", s.formatCommentSummary(), s.formatCommentDetails())) lines = append(lines, fmt.Sprintf("<i>Комментарий:</i> <b>%s</b>%s", s.formatCommentSummary(), s.formatCommentDetails()))
} }
@@ -449,8 +480,7 @@ func (s *PurchaseOptionalDetails) formatDateTime(value *time.Time) string {
return "—" return "—"
} }
local := value.In(ui2.MskLocation) local := value.In(ui2.MskLocation)
text := fmt.Sprintf("%s %02d %s %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()), local.Format("15:04")) return fmt.Sprintf("%s %02d %s %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()), local.Format("15:04"))
return fmt.Sprintf("<b>%s</b>", text)
} }
func (s *PurchaseOptionalDetails) formatDate(value *time.Time) string { func (s *PurchaseOptionalDetails) formatDate(value *time.Time) string {
@@ -458,23 +488,21 @@ func (s *PurchaseOptionalDetails) formatDate(value *time.Time) string {
return "—" return "—"
} }
local := value.In(ui2.MskLocation) local := value.In(ui2.MskLocation)
text := fmt.Sprintf("%s %02d %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month())) return fmt.Sprintf("%s %02d %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()))
return fmt.Sprintf("<b>%s</b>", text)
} }
func (s *PurchaseOptionalDetails) formatText(value string) string { func (s *PurchaseOptionalDetails) formatText(value string) string {
if value == "" { if value == "" {
return "—" return "—"
} }
return fmt.Sprintf("<b>%s</b>", value) return value
} }
func (s *PurchaseOptionalDetails) formatCostValue() string { func (s *PurchaseOptionalDetails) formatCostValue() string {
if s.CostValue == nil { if s.CostValue == nil {
return "—" return "—"
} }
text := fmt.Sprintf("%s %.0f₽", s.costTypeLabel(), *s.CostValue) return fmt.Sprintf("%s %.0f₽", s.costTypeLabel(), *s.CostValue)
return fmt.Sprintf("<b>%s</b>", text)
} }
func (s *PurchaseOptionalDetails) formatCostBefore() string { func (s *PurchaseOptionalDetails) formatCostBefore() string {
@@ -482,8 +510,7 @@ func (s *PurchaseOptionalDetails) formatCostBefore() string {
return "—" return "—"
} }
label := s.costBeforeTypeLabelForEntry(*s.CostBeforeBargain) label := s.costBeforeTypeLabelForEntry(*s.CostBeforeBargain)
text := fmt.Sprintf("%s %.0f₽", label, *s.CostBeforeBargain.Value) return fmt.Sprintf("%s %.0f₽", label, *s.CostBeforeBargain.Value)
return fmt.Sprintf("<b>%s</b>", text)
} }
func (s *PurchaseOptionalDetails) costTypeLabel() string { func (s *PurchaseOptionalDetails) costTypeLabel() string {
@@ -576,6 +603,22 @@ func (s *PurchaseOptionalDetails) commentButton() echotron.InlineKeyboardButton
return Button(fmt.Sprintf("%s Комментарий%s", icon, modeIcon), "opt_comment") return Button(fmt.Sprintf("%s Комментарий%s", icon, modeIcon), "opt_comment")
} }
func (s *PurchaseOptionalDetails) inviteLinkTypeButton() echotron.InlineKeyboardButton {
icon := ""
if s.hasInviteLinkTypeValue() {
icon = "✎"
}
modeIcon := ""
if len(s.Channels) > 1 && s.InviteLinkTypeMode == "per_channel" {
modeIcon = " 👥"
}
label := "Тип ссылки"
if s.InviteLinkType != "" {
label += ": " + s.inviteLinkTypeLabel(s.InviteLinkType)
}
return Button(fmt.Sprintf("%s %s%s", icon, label, modeIcon), "opt_invite_link_type")
}
func (s *PurchaseOptionalDetails) globalModeButton() echotron.InlineKeyboardButton { func (s *PurchaseOptionalDetails) globalModeButton() echotron.InlineKeyboardButton {
return Button("⚙️ Режимы параметров", "toggle_global_mode") return Button("⚙️ Режимы параметров", "toggle_global_mode")
} }
@@ -686,6 +729,25 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
return return
case "format_custom": case "format_custom":
text += "<b>Введите формат</b>\n\nНапример: <code>пост</code>" text += "<b>Введите формат</b>\n\nНапример: <code>пост</code>"
case "invite_link_type":
text = "<i>Страница выбора типа ссылки</i>\n\n"
if s.CurrentChannel != "" {
text += fmt.Sprintf("Канал: <b>%s</b>\n\n", channelLabelByKey(s.Channels, s.CurrentChannel))
}
text += "Выберите тип ссылки\n\n"
keyboard := Keyboard(
Row(
Button("Открытая", "invite_link_type:public"),
Button("С заявками", "invite_link_type:approval"),
),
Row(Button("← Назад", s.backAction())),
)
if mode == bot.EditMessage {
b.Edit(text, keyboard)
} else {
b.SendNew(text, keyboard)
}
return
case "comment": case "comment":
text += "<b>Введите комментарий</b>" text += "<b>Введите комментарий</b>"
default: default:
@@ -761,6 +823,15 @@ func (s *PurchaseOptionalDetails) renderModeSelect(b *bot.Bot, mode bot.RenderMo
} }
rows = append(rows, Row(Button(formatLabel, "toggle_param_mode:format"))) rows = append(rows, Row(Button(formatLabel, "toggle_param_mode:format")))
// Тип ссылки
inviteLinkTypeLabel := "Тип ссылки: "
if s.InviteLinkTypeMode == "per_channel" {
inviteLinkTypeLabel += "👥"
} else {
inviteLinkTypeLabel += "общий"
}
rows = append(rows, Row(Button(inviteLinkTypeLabel, "toggle_param_mode:invite_link_type")))
rows = append(rows, Row(Button("← Назад", "back_to_optional"))) rows = append(rows, Row(Button("← Назад", "back_to_optional")))
b.Render(text, Keyboard(rows...), mode) b.Render(text, Keyboard(rows...), mode)
@@ -789,6 +860,9 @@ func (s *PurchaseOptionalDetails) renderParamScreens(b *bot.Bot, mode bot.Render
case "format_channels": case "format_channels":
s.renderParamChannels(b, mode, "format") s.renderParamChannels(b, mode, "format")
return true return true
case "invite_link_type_channels":
s.renderParamChannels(b, mode, "invite_link_type")
return true
default: default:
return false return false
} }
@@ -928,7 +1002,7 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
// Паддинг вычисляем так, чтобы все значения начинались с одной позиции // Паддинг вычисляем так, чтобы все значения начинались с одной позиции
const ( const (
marker = "· " // Маркер пункта marker = "· " // Маркер пункта
valueIndent = 2 // Отступ после самого длинного названия valueIndent = 6 // Отступ после самого длинного названия (увеличен для лучшей читаемости)
) )
var result []string var result []string
@@ -936,7 +1010,7 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
labelLen := len([]rune(line.label)) labelLen := len([]rune(line.label))
// Паддинг = (макс.длина - тек.длина) + отступ после названия // Паддинг = (макс.длина - тек.длина) + отступ после названия
paddingLen := maxLabelLen - labelLen + valueIndent paddingLen := maxLabelLen - labelLen + valueIndent
padding := strings.Repeat(" ", paddingLen) padding := strings.Repeat("\u00A0", paddingLen)
formattedLine := fmt.Sprintf("%s%s%s%s", marker, line.label, padding, line.value) formattedLine := fmt.Sprintf("%s%s%s%s", marker, line.label, padding, line.value)
result = append(result, formattedLine) result = append(result, formattedLine)
@@ -953,6 +1027,9 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
finalResult := strings.Join(result, "\n") finalResult := strings.Join(result, "\n")
// Оборачиваем в <pre> для моноширинного шрифта (выравнивание работает только в monospace)
finalResult = fmt.Sprintf("<pre>%s</pre>", finalResult)
// Логируем итоговый результат // Логируем итоговый результат
log.Info(). log.Info().
Str("param", param). Str("param", param).
@@ -1100,6 +1177,11 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
s.ReturnMode = "" s.ReturnMode = ""
s.CurrentChannel = "" s.CurrentChannel = ""
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
case "invite_link_type":
s.InputMode = "invite_link_type"
s.ReturnMode = ""
s.CurrentChannel = ""
s.Enter(b, bot.EditMessage)
default: default:
s.InputMode = "" s.InputMode = ""
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
@@ -1151,6 +1233,11 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, channelKe
s.ReturnMode = "format_channels" s.ReturnMode = "format_channels"
s.CurrentChannel = channelKey s.CurrentChannel = channelKey
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
case "invite_link_type":
s.InputMode = "invite_link_type"
s.ReturnMode = "invite_link_type_channels"
s.CurrentChannel = channelKey
s.Enter(b, bot.EditMessage)
default: default:
s.InputMode = "" s.InputMode = ""
s.Enter(b, bot.EditMessage) s.Enter(b, bot.EditMessage)
@@ -1204,6 +1291,13 @@ func (s *PurchaseOptionalDetails) copyParamValue(param, channelKey string) {
} else { } else {
s.FormatCopy = nil s.FormatCopy = nil
} }
case "invite_link_type":
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok {
copied := value
s.InviteLinkTypeCopy = &copied
} else {
s.InviteLinkTypeCopy = nil
}
} }
} }
@@ -1275,6 +1369,14 @@ func (s *PurchaseOptionalDetails) copyCommonValueToChannels(param string) {
key := channelKey(ch) key := channelKey(ch)
s.FormatByChannel[key] = s.Format s.FormatByChannel[key] = s.Format
} }
case "invite_link_type":
if s.InviteLinkType == "" {
return
}
for _, ch := range s.Channels {
key := channelKey(ch)
s.InviteLinkTypeByChannel[key] = s.InviteLinkType
}
} }
} }
@@ -1334,6 +1436,12 @@ func (s *PurchaseOptionalDetails) pasteParamValue(param, channelKey string) {
return return
} }
s.FormatByChannel[channelKey] = *s.FormatCopy s.FormatByChannel[channelKey] = *s.FormatCopy
case "invite_link_type":
if s.InviteLinkTypeCopy == nil {
delete(s.InviteLinkTypeByChannel, channelKey)
return
}
s.InviteLinkTypeByChannel[channelKey] = *s.InviteLinkTypeCopy
} }
} }
@@ -1367,6 +1475,10 @@ func (s *PurchaseOptionalDetails) applyParamToAll(param string) {
for _, ch := range s.Channels { for _, ch := range s.Channels {
s.pasteParamValue(param, channelKey(ch)) s.pasteParamValue(param, channelKey(ch))
} }
case "invite_link_type":
for _, ch := range s.Channels {
s.pasteParamValue(param, channelKey(ch))
}
} }
} }
@@ -1386,6 +1498,8 @@ func (s *PurchaseOptionalDetails) clearParamValue(param, channelKey string) {
delete(s.CommentByChannel, channelKey) delete(s.CommentByChannel, channelKey)
case "format": case "format":
delete(s.FormatByChannel, channelKey) delete(s.FormatByChannel, channelKey)
case "invite_link_type":
delete(s.InviteLinkTypeByChannel, channelKey)
} }
} }
@@ -1405,6 +1519,8 @@ func (s *PurchaseOptionalDetails) paramTitle(param string) string {
return "Комментарий" return "Комментарий"
case "format": case "format":
return "Формат" return "Формат"
case "invite_link_type":
return "Тип ссылки"
default: default:
return "" return ""
} }
@@ -1447,6 +1563,11 @@ func (s *PurchaseOptionalDetails) paramMode(param string) string {
return "common" return "common"
} }
return s.FormatMode return s.FormatMode
case "invite_link_type":
if s.InviteLinkTypeMode == "" {
return "common"
}
return s.InviteLinkTypeMode
default: default:
return "common" return "common"
} }
@@ -1468,6 +1589,8 @@ func (s *PurchaseOptionalDetails) setParamMode(param, mode string) {
s.CommentMode = mode s.CommentMode = mode
case "format": case "format":
s.FormatMode = mode s.FormatMode = mode
case "invite_link_type":
s.InviteLinkTypeMode = mode
} }
} }
@@ -1493,6 +1616,9 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
if s.FormatMode == "" { if s.FormatMode == "" {
s.FormatMode = "common" s.FormatMode = "common"
} }
if s.InviteLinkTypeMode == "" {
s.InviteLinkTypeMode = "common"
}
if s.PlacementByChannel == nil { if s.PlacementByChannel == nil {
s.PlacementByChannel = make(map[string]*time.Time) s.PlacementByChannel = make(map[string]*time.Time)
} }
@@ -1514,6 +1640,9 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
if s.FormatByChannel == nil { if s.FormatByChannel == nil {
s.FormatByChannel = make(map[string]string) s.FormatByChannel = make(map[string]string)
} }
if s.InviteLinkTypeByChannel == nil {
s.InviteLinkTypeByChannel = make(map[string]string)
}
s.syncChannelMaps() s.syncChannelMaps()
} }
@@ -1560,6 +1689,11 @@ func (s *PurchaseOptionalDetails) syncChannelMaps() {
delete(s.FormatByChannel, key) delete(s.FormatByChannel, key)
} }
} }
for key := range s.InviteLinkTypeByChannel {
if _, ok := valid[key]; !ok {
delete(s.InviteLinkTypeByChannel, key)
}
}
} }
func (s *PurchaseOptionalDetails) formatPlacementSummary() string { func (s *PurchaseOptionalDetails) formatPlacementSummary() string {
@@ -1573,7 +1707,7 @@ func (s *PurchaseOptionalDetails) formatPlacementDetails() string {
if s.PlacementMode != "per_channel" || len(s.Channels) <= 1 { if s.PlacementMode != "per_channel" || len(s.Channels) <= 1 {
return "" return ""
} }
return "\n" + s.renderParamChannelSummary("placement") return s.renderParamChannelSummary("placement")
} }
func (s *PurchaseOptionalDetails) formatPaymentDateSummary() string { func (s *PurchaseOptionalDetails) formatPaymentDateSummary() string {
@@ -1587,7 +1721,7 @@ func (s *PurchaseOptionalDetails) formatPaymentDateDetails() string {
if s.PaymentDateMode != "per_channel" || len(s.Channels) <= 1 { if s.PaymentDateMode != "per_channel" || len(s.Channels) <= 1 {
return "" return ""
} }
return "\n" + s.renderParamChannelSummary("payment_date") return s.renderParamChannelSummary("payment_date")
} }
func (s *PurchaseOptionalDetails) formatCostSummary() string { func (s *PurchaseOptionalDetails) formatCostSummary() string {
@@ -1601,7 +1735,7 @@ func (s *PurchaseOptionalDetails) formatCostDetails() string {
if s.CostMode != "per_channel" || len(s.Channels) <= 1 { if s.CostMode != "per_channel" || len(s.Channels) <= 1 {
return "" return ""
} }
return "\n" + s.renderParamChannelSummary("cost") return s.renderParamChannelSummary("cost")
} }
func (s *PurchaseOptionalDetails) formatCostBeforeSummary() string { func (s *PurchaseOptionalDetails) formatCostBeforeSummary() string {
@@ -1615,7 +1749,7 @@ func (s *PurchaseOptionalDetails) formatCostBeforeDetails() string {
if s.CostBeforeMode != "per_channel" || len(s.Channels) <= 1 { if s.CostBeforeMode != "per_channel" || len(s.Channels) <= 1 {
return "" return ""
} }
return "\n" + s.renderParamChannelSummary("cost_before") return s.renderParamChannelSummary("cost_before")
} }
func (s *PurchaseOptionalDetails) formatPurchaseTypeSummary() string { func (s *PurchaseOptionalDetails) formatPurchaseTypeSummary() string {
@@ -1629,7 +1763,7 @@ func (s *PurchaseOptionalDetails) formatPurchaseTypeDetails() string {
if s.PurchaseTypeMode != "per_channel" || len(s.Channels) <= 1 { if s.PurchaseTypeMode != "per_channel" || len(s.Channels) <= 1 {
return "" return ""
} }
return "\n" + s.renderParamChannelSummary("purchase_type") return s.renderParamChannelSummary("purchase_type")
} }
func (s *PurchaseOptionalDetails) formatCommentSummary() string { func (s *PurchaseOptionalDetails) formatCommentSummary() string {
@@ -1643,7 +1777,7 @@ func (s *PurchaseOptionalDetails) formatCommentDetails() string {
if s.CommentMode != "per_channel" || len(s.Channels) <= 1 { if s.CommentMode != "per_channel" || len(s.Channels) <= 1 {
return "" return ""
} }
return "\n" + s.renderParamChannelSummary("comment") return s.renderParamChannelSummary("comment")
} }
func (s *PurchaseOptionalDetails) formatFormatSummary() string { func (s *PurchaseOptionalDetails) formatFormatSummary() string {
@@ -1657,7 +1791,21 @@ func (s *PurchaseOptionalDetails) formatFormatDetails() string {
if s.FormatMode != "per_channel" || len(s.Channels) <= 1 { if s.FormatMode != "per_channel" || len(s.Channels) <= 1 {
return "" return ""
} }
return "\n" + s.renderParamChannelSummary("format") return s.renderParamChannelSummary("format")
}
func (s *PurchaseOptionalDetails) formatInviteLinkTypeSummary() string {
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
return "👥"
}
return s.inviteLinkTypeLabel(s.InviteLinkType)
}
func (s *PurchaseOptionalDetails) formatInviteLinkTypeDetails() string {
if s.InviteLinkTypeMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return s.renderParamChannelSummary("invite_link_type")
} }
func (s *PurchaseOptionalDetails) formatParamValue(param, channelKey string) string { func (s *PurchaseOptionalDetails) formatParamValue(param, channelKey string) string {
@@ -1674,17 +1822,22 @@ func (s *PurchaseOptionalDetails) formatParamValue(param, channelKey string) str
return s.formatCostBeforeForChannel(channelKey) return s.formatCostBeforeForChannel(channelKey)
case "purchase_type": case "purchase_type":
if value, ok := s.PurchaseTypeByChannel[channelKey]; ok && value != "" { if value, ok := s.PurchaseTypeByChannel[channelKey]; ok && value != "" {
return fmt.Sprintf("<b>%s</b>", value) return value
} }
return "—" return "—"
case "comment": case "comment":
if value, ok := s.CommentByChannel[channelKey]; ok && value != "" { if value, ok := s.CommentByChannel[channelKey]; ok && value != "" {
return fmt.Sprintf("<b>%s</b>", value) return value
} }
return "—" return "—"
case "format": case "format":
if value, ok := s.FormatByChannel[channelKey]; ok && value != "" { if value, ok := s.FormatByChannel[channelKey]; ok && value != "" {
return fmt.Sprintf("<b>%s</b>", value) return value
}
return "—"
case "invite_link_type":
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" {
return s.inviteLinkTypeLabel(value)
} }
return "—" return "—"
default: default:
@@ -1698,8 +1851,7 @@ func (s *PurchaseOptionalDetails) formatCostForChannel(channelKey string) string
return "—" return "—"
} }
label := s.costTypeLabelForEntry(entry) label := s.costTypeLabelForEntry(entry)
text := fmt.Sprintf("%s %.0f₽", label, *entry.Value) return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
return fmt.Sprintf("<b>%s</b>", text)
} }
func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string) string { func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string) string {
@@ -1708,8 +1860,7 @@ func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string)
return "—" return "—"
} }
label := s.costBeforeTypeLabelForEntry(entry) label := s.costBeforeTypeLabelForEntry(entry)
text := fmt.Sprintf("%s %.0f₽", label, *entry.Value) return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
return fmt.Sprintf("<b>%s</b>", text)
} }
func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool { func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
@@ -1728,6 +1879,8 @@ func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
return s.CommentCopy != nil return s.CommentCopy != nil
case "format": case "format":
return s.FormatCopy != nil return s.FormatCopy != nil
case "invite_link_type":
return s.InviteLinkTypeCopy != nil
default: default:
return false return false
} }
@@ -1756,6 +1909,9 @@ func (s *PurchaseOptionalDetails) hasChannelValue(param, channelKey string) bool
case "format": case "format":
value, ok := s.FormatByChannel[channelKey] value, ok := s.FormatByChannel[channelKey]
return ok && value != "" return ok && value != ""
case "invite_link_type":
value, ok := s.InviteLinkTypeByChannel[channelKey]
return ok && value != ""
default: default:
return false return false
} }
@@ -1845,6 +2001,28 @@ func (s *PurchaseOptionalDetails) hasCommentValue() bool {
return s.Comment != "" return s.Comment != ""
} }
func (s *PurchaseOptionalDetails) hasInviteLinkTypeValue() bool {
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
for _, value := range s.InviteLinkTypeByChannel {
if value != "" {
return true
}
}
return false
}
return s.InviteLinkType != ""
}
func (s *PurchaseOptionalDetails) inviteLinkTypeLabel(linkType string) string {
if linkType == "public" {
return "Открытая"
}
if linkType == "approval" {
return "С заявками"
}
return "—"
}
func (s *PurchaseOptionalDetails) costTypeLabelForEntry(entry CostEntry) string { func (s *PurchaseOptionalDetails) costTypeLabelForEntry(entry CostEntry) string {
if entry.Type == "cpm" { if entry.Type == "cpm" {
return "СРМ" return "СРМ"
@@ -1966,6 +2144,14 @@ func (s *PurchaseOptionalDetails) setFormatValue(value string) {
s.FormatByChannel[s.CurrentChannel] = value s.FormatByChannel[s.CurrentChannel] = value
} }
func (s *PurchaseOptionalDetails) setInviteLinkTypeValue(value string) {
if s.CurrentChannel == "" {
s.InviteLinkType = value
return
}
s.InviteLinkTypeByChannel[s.CurrentChannel] = value
}
func (s *PurchaseOptionalDetails) backAction() string { func (s *PurchaseOptionalDetails) backAction() string {
if s.ReturnMode != "" { if s.ReturnMode != "" {
return "back_to_return" return "back_to_return"
@@ -2081,6 +2267,11 @@ func (s *PurchaseOptionalDetails) buildPlacementDetails(placementType *string) *
details.Format = &s.Format details.Format = &s.Format
} }
} }
if s.InviteLinkTypeMode != "per_channel" || len(s.Channels) <= 1 {
if s.InviteLinkType != "" {
details.InviteLinkType = &s.InviteLinkType
}
}
if placementType != nil && (s.PurchaseTypeMode == "per_channel" || len(s.Channels) <= 1) { if placementType != nil && (s.PurchaseTypeMode == "per_channel" || len(s.Channels) <= 1) {
details.PlacementType = placementType details.PlacementType = placementType
} }
@@ -2130,6 +2321,11 @@ func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string) *backen
details.Format = &value details.Format = &value
} }
} }
if s.InviteLinkTypeMode == "per_channel" {
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" {
details.InviteLinkType = &value
}
}
return details return details
} }