diff --git a/src/controller/http_v1/internal.py b/src/controller/http_v1/internal.py
index 4d66193..cece2f7 100644
--- a/src/controller/http_v1/internal.py
+++ b/src/controller/http_v1/internal.py
@@ -123,9 +123,12 @@ async def _handle_chat_join_request(event: dict) -> None:
return
from_user = event.get('from')
- if not from_user:
- log.error('No user in chat_join_request event')
- return
+ if not from_user or not from_user.get('id'):
+ user_chat_id = event.get('user_chat_id')
+ if not user_chat_id:
+ log.error('No user or user_chat_id in chat_join_request event')
+ return
+ from_user = {'id': user_chat_id}
invite_link_obj = event.get('invite_link', {})
invite_link = invite_link_obj.get('invite_link')
@@ -153,7 +156,8 @@ async def _handle_chat_member_updated(event: dict) -> None:
return
from_user = event.get('from')
- if not from_user:
+ member_user = event.get('new_chat_member', {}).get('user')
+ if not member_user:
log.error('No user in chat_member_updated event')
return
@@ -181,16 +185,16 @@ async def _handle_chat_member_updated(event: dict) -> None:
return
await deps.get_usecase().handle_subscription(
- user_telegram_id=from_user['id'],
- username=from_user.get('username'),
+ user_telegram_id=member_user['id'],
+ username=member_user.get('username'),
invite_link=invite_link,
- first_name=from_user.get('first_name'),
- last_name=from_user.get('last_name'),
+ first_name=member_user.get('first_name'),
+ last_name=member_user.get('last_name'),
)
elif user_left:
await deps.get_usecase().handle_unsubscription(
- user_telegram_id=from_user['id'],
+ user_telegram_id=member_user['id'],
channel_telegram_id=chat['id'],
)
diff --git a/src/usecase/placement/create_placement.py b/src/usecase/placement/create_placement.py
index 74ef62f..69e36d2 100644
--- a/src/usecase/placement/create_placement.py
+++ b/src/usecase/placement/create_placement.py
@@ -42,6 +42,13 @@ async def create_placement(
raise domain.ChannelNotFound(project.channel.id)
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
+ log.info(
+ 'Created invite link for project=%s channel_telegram_id=%s requires_approval=%s invite_link=%s',
+ project.id,
+ project.channel.telegram_id,
+ requires_approval,
+ invite_link,
+ )
placement = domain.Placement(
project_id=input.project_id,
diff --git a/src/usecase/placement/fetch_placement_post_cycle.py b/src/usecase/placement/fetch_placement_post_cycle.py
index 0a3a4c8..1251642 100644
--- a/src/usecase/placement/fetch_placement_post_cycle.py
+++ b/src/usecase/placement/fetch_placement_post_cycle.py
@@ -64,4 +64,4 @@ async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) ->
post.id,
)
- log.info('Fetch placement post cycle completed. Created %s placements', created_count)
+ log.debug('Fetch placement post cycle completed. Created %s placements', created_count)
diff --git a/src/usecase/project/tg_add_project.py b/src/usecase/project/tg_add_project.py
index 917bd5d..d78453a 100644
--- a/src/usecase/project/tg_add_project.py
+++ b/src/usecase/project/tg_add_project.py
@@ -121,6 +121,7 @@ async def tg_add_project(self: 'Usecase', input: dto.ConnectProjectInput) -> dto
title=channel.title,
username=channel.username,
status=project.status,
+ purchase_invite_type_default=project.purchase_invite_type_default,
)
# Если >1 workspace - создаем/обновляем канал и отправляем уведомление
diff --git a/src/usecase/purchase/create_purchase.py b/src/usecase/purchase/create_purchase.py
index 11b991e..cdb275b 100644
--- a/src/usecase/purchase/create_purchase.py
+++ b/src/usecase/purchase/create_purchase.py
@@ -97,11 +97,6 @@ async def create_purchase(
if not creative or creative.project_id != project.id:
raise domain.CreativeNotFound(input.creative_id)
- existing_purchase = await self.database.get_active_purchase(project.id, creative.id)
- if existing_purchase:
- channels = await self.database.get_purchase_channels(existing_purchase.id)
- return _build_purchase_output(existing_purchase, channels)
-
details = input.details
purchase = domain.Purchase(
workspace_id=workspace_id,
@@ -143,6 +138,13 @@ async def create_purchase(
log.info('Created channel @%s with telegram_id=%s', channel.username, channel.telegram_id)
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
+ log.info(
+ 'Created invite link for purchase %s channel_telegram_id=%s requires_approval=%s invite_link=%s',
+ purchase.id,
+ project.channel.telegram_id,
+ requires_approval,
+ invite_link,
+ )
channel_details = channel_input.details
purchase_channel = await self.database.add_channel_to_purchase(
diff --git a/telegram_bot/bot/bot.go b/telegram_bot/bot/bot.go
index 75c1309..0c49632 100644
--- a/telegram_bot/bot/bot.go
+++ b/telegram_bot/bot/bot.go
@@ -2,6 +2,7 @@ package bot
import (
"context"
+ "encoding/json"
"fmt"
"strings"
"sync"
@@ -222,6 +223,11 @@ func (b *Bot) Update(u *echotron.Update) {
if u.ChatJoinRequest != nil || u.ChatMember != nil || u.MyChatMember != nil {
go func() {
ctx := context.Background()
+ if raw, err := json.Marshal(u); err == nil {
+ log.Info().RawJSON("update", raw).Msg("System event payload")
+ } else {
+ log.Error().Err(err).Msg("Failed to marshal system event payload")
+ }
err := b.Backend.ForwardTelegramUpdate(ctx, u)
if err != nil {
log.Error().Err(err).Msg("Failed to forward system event to backend")
diff --git a/telegram_bot/screens/purchase_optional_details.go b/telegram_bot/screens/purchase_optional_details.go
index c67ab4c..236c125 100644
--- a/telegram_bot/screens/purchase_optional_details.go
+++ b/telegram_bot/screens/purchase_optional_details.go
@@ -1433,6 +1433,7 @@ func (s *PurchaseOptionalDetails) sendCreativeWithInviteLink(
creative *backend.Creative,
inviteLink string,
) {
+ log.Info().Str("invite_link", inviteLink).Msg("Sending creative with invite link")
linkedText := s.injectInviteLink(creative.Text, inviteLink)
editor := CreativeEditorFields{
Text: &linkedText,
diff --git a/tg_parser/internal/adapter/telegram/get_channel_diff.go b/tg_parser/internal/adapter/telegram/get_channel_diff.go
index 394e74f..3161498 100644
--- a/tg_parser/internal/adapter/telegram/get_channel_diff.go
+++ b/tg_parser/internal/adapter/telegram/get_channel_diff.go
@@ -59,7 +59,8 @@ func extractPosts(channel domain.Channel, msgs []tg.MessageClass) []domain.Post
continue
}
- p := domain.NewPost(channel, m.ID, m.Message, m.Views)
+ text := messageToHTML(m.Message, m.Entities)
+ p := domain.NewPost(channel, m.ID, text, m.Views)
posts = append(posts, p)
}
diff --git a/tg_parser/internal/adapter/telegram/history.go b/tg_parser/internal/adapter/telegram/history.go
index 7ec1e62..cdfd68e 100644
--- a/tg_parser/internal/adapter/telegram/history.go
+++ b/tg_parser/internal/adapter/telegram/history.go
@@ -38,7 +38,8 @@ func (t *Telegram) GetChannelHistory(ctx context.Context, channel domain.Channel
continue
}
- p := domain.NewPost(channel, m.ID, m.Message, m.Views)
+ text := messageToHTML(m.Message, m.Entities)
+ p := domain.NewPost(channel, m.ID, text, m.Views)
placements = append(placements, p)
}
diff --git a/tg_parser/internal/adapter/telegram/message_html.go b/tg_parser/internal/adapter/telegram/message_html.go
new file mode 100644
index 0000000..3ddc4e5
--- /dev/null
+++ b/tg_parser/internal/adapter/telegram/message_html.go
@@ -0,0 +1,197 @@
+package telegram
+
+import (
+ "html"
+ "sort"
+ "strconv"
+ "strings"
+ "unicode/utf8"
+
+ "github.com/gotd/td/tg"
+)
+
+type htmlEntity struct {
+ start int
+ end int
+ openTag string
+ closeTag string
+ length int
+}
+
+type htmlEvent struct {
+ pos int
+ tag string
+ isStart bool
+ length int
+}
+
+func messageToHTML(text string, entities []tg.MessageEntityClass) string {
+ if text == "" {
+ return ""
+ }
+
+ if len(entities) == 0 {
+ return html.EscapeString(text)
+ }
+
+ ranges := make([]htmlEntity, 0, len(entities))
+ needed := make([]int, 0, len(entities)*2)
+
+ for _, raw := range entities {
+ offset, length, openTag, closeTag, ok := htmlEntityMeta(raw)
+ if !ok {
+ continue
+ }
+
+ start := offset
+ end := offset + length
+ if length <= 0 {
+ continue
+ }
+
+ ranges = append(ranges, htmlEntity{
+ start: start,
+ end: end,
+ openTag: openTag,
+ closeTag: closeTag,
+ length: length,
+ })
+ needed = append(needed, start, end)
+ }
+
+ if len(ranges) == 0 {
+ return html.EscapeString(text)
+ }
+
+ positions := utf16PositionsToBytes(text, needed)
+ events := make([]htmlEvent, 0, len(ranges)*2)
+
+ for _, r := range ranges {
+ startByte, okStart := positions[r.start]
+ endByte, okEnd := positions[r.end]
+ if !okStart || !okEnd || startByte > endByte {
+ continue
+ }
+
+ events = append(events, htmlEvent{
+ pos: startByte,
+ tag: r.openTag,
+ isStart: true,
+ length: r.length,
+ })
+ events = append(events, htmlEvent{
+ pos: endByte,
+ tag: r.closeTag,
+ isStart: false,
+ length: r.length,
+ })
+ }
+
+ sort.SliceStable(events, func(i, j int) bool {
+ if events[i].pos != events[j].pos {
+ return events[i].pos < events[j].pos
+ }
+ if events[i].isStart != events[j].isStart {
+ return !events[i].isStart
+ }
+ if events[i].isStart {
+ return events[i].length > events[j].length
+ }
+ return events[i].length < events[j].length
+ })
+
+ var b strings.Builder
+ last := 0
+ for _, ev := range events {
+ if ev.pos > last {
+ b.WriteString(html.EscapeString(text[last:ev.pos]))
+ }
+ b.WriteString(ev.tag)
+ last = ev.pos
+ }
+ if last < len(text) {
+ b.WriteString(html.EscapeString(text[last:]))
+ }
+
+ return b.String()
+}
+
+func htmlEntityMeta(entity tg.MessageEntityClass) (offset int, length int, openTag string, closeTag string, ok bool) {
+ switch e := entity.(type) {
+ case *tg.MessageEntityBold:
+ return e.Offset, e.Length, "", "", true
+ case *tg.MessageEntityItalic:
+ return e.Offset, e.Length, "", "", true
+ case *tg.MessageEntityUnderline:
+ return e.Offset, e.Length, "", "", true
+ case *tg.MessageEntityStrike:
+ return e.Offset, e.Length, "", "", true
+ case *tg.MessageEntityCode:
+ return e.Offset, e.Length, "", "", true
+ case *tg.MessageEntityPre:
+ if e.Language != "" {
+ lang := html.EscapeString(e.Language)
+ return e.Offset, e.Length, `
`, "", true
+ }
+ return e.Offset, e.Length, "", "", true + case *tg.MessageEntityTextURL: + url := html.EscapeString(e.URL) + return e.Offset, e.Length, ``, "", true + case *tg.MessageEntityMentionName: + userID := html.EscapeString(strconv.FormatInt(e.UserID, 10)) + return e.Offset, e.Length, ``, "", true + case *tg.MessageEntitySpoiler: + return e.Offset, e.Length, ``, "", true + case *tg.MessageEntityBlockquote: + if e.Collapsed { + return e.Offset, e.Length, `
`, "", true + } + return e.Offset, e.Length, "
", "", true + case *tg.MessageEntityCustomEmoji: + id := html.EscapeString(strconv.FormatInt(e.DocumentID, 10)) + return e.Offset, e.Length, `