From b41ce6293efafc8b8a723e93440437e0d3adfb4d Mon Sep 17 00:00:00 2001 From: Magzhan Zhumabayev Date: Tue, 12 May 2026 19:19:24 +0500 Subject: [PATCH] . --- .../service/FacebookPostingService.java | 20 +---- .../service/LinkedInPostingService.java | 32 +------- .../service/SocialPostTextFormatter.java | 82 +++++++++++++++++++ .../service/TelegramPostingService.java | 32 +------- .../service/SocialPostTextFormatterTest.java | 42 ++++++++++ 5 files changed, 136 insertions(+), 72 deletions(-) create mode 100644 src/main/java/kz/konturai/parser/service/SocialPostTextFormatter.java create mode 100644 src/test/java/kz/konturai/parser/service/SocialPostTextFormatterTest.java diff --git a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java index 7935126..0cbe539 100644 --- a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java +++ b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java @@ -18,7 +18,6 @@ import reactor.netty.http.client.HttpClient; import java.time.Duration; import java.util.List; -import java.util.stream.Collectors; @Service @Slf4j @@ -95,7 +94,7 @@ public class FacebookPostingService { private String postTextOnly(PageCredentials creds, String postText, List hashtags) { validateToken(creds); try { - String fullText = buildPostText(postText, hashtags); + String fullText = SocialPostTextFormatter.buildPostText(postText, hashtags); MultiValueMap formData = new LinkedMultiValueMap<>(); formData.add("access_token", creds.pageToken); @@ -124,7 +123,7 @@ public class FacebookPostingService { private String postWithPhoto(PageCredentials creds, String postText, List hashtags, byte[] imageData) { validateToken(creds); try { - String fullText = buildPostText(postText, hashtags); + String fullText = SocialPostTextFormatter.buildPostText(postText, hashtags); MultiValueMap formData = new LinkedMultiValueMap<>(); formData.add("access_token", creds.pageToken); @@ -157,7 +156,7 @@ public class FacebookPostingService { private String postWithVideo(PageCredentials creds, String postText, List hashtags, byte[] videoData) { validateToken(creds); try { - String fullText = buildPostText(postText, hashtags); + String fullText = SocialPostTextFormatter.buildPostText(postText, hashtags); log.info("[Facebook] Загружаю видео размером {} MB для страницы {}", String.format("%.1f", videoData.length / 1024.0 / 1024.0), creds.pageId); @@ -294,17 +293,6 @@ public class FacebookPostingService { } } - private String buildPostText(String postText, List hashtags) { - StringBuilder sb = new StringBuilder(postText != null ? postText : ""); - if (hashtags != null && !hashtags.isEmpty()) { - if (sb.length() > 0) sb.append("\n\n"); - sb.append(hashtags.stream() - .map(t -> t.startsWith("#") ? t : "#" + t) - .collect(Collectors.joining(" "))); - } - return sb.toString(); - } - /** * Извлекаем короткий заголовок из текста поста (первые 100 символов до точки/переноса) */ @@ -345,4 +333,4 @@ public class FacebookPostingService { this.pageToken = pageToken; } } -} \ No newline at end of file +} diff --git a/src/main/java/kz/konturai/parser/service/LinkedInPostingService.java b/src/main/java/kz/konturai/parser/service/LinkedInPostingService.java index ee9c98b..f446bd4 100644 --- a/src/main/java/kz/konturai/parser/service/LinkedInPostingService.java +++ b/src/main/java/kz/konturai/parser/service/LinkedInPostingService.java @@ -19,7 +19,6 @@ import reactor.netty.http.client.HttpClient; import java.time.Duration; import java.util.List; -import java.util.stream.Collectors; @Service public class LinkedInPostingService { @@ -58,8 +57,8 @@ public class LinkedInPostingService { */ public String postToLinkedIn(String accessToken, String postText, List hashtags) { try { - // Формируем полный текст поста с хештегами - String fullPostText = buildPostText(postText, hashtags); + // Формируем полный текст поста с контактами и хештегами + String fullPostText = SocialPostTextFormatter.buildPostText(postText, hashtags); // Получаем URN пользователя String personUrn = getPersonUrn(accessToken); @@ -99,8 +98,8 @@ public class LinkedInPostingService { public String postToLinkedInWithImage(String accessToken, String postText, List hashtags, byte[] imageData, String imageContentType) { try { - // Формируем полный текст поста с хештегами - String fullPostText = buildPostText(postText, hashtags); + // Формируем полный текст поста с контактами и хештегами + String fullPostText = SocialPostTextFormatter.buildPostText(postText, hashtags); // Получаем URN пользователя String personUrn = getPersonUrn(accessToken); @@ -301,28 +300,6 @@ public class LinkedInPostingService { } } - /** - * Формирует полный текст поста с хештегами - */ - private String buildPostText(String postText, List hashtags) { - StringBuilder fullText = new StringBuilder(postText != null ? postText : ""); - - if (hashtags != null && !hashtags.isEmpty()) { - if (fullText.length() > 0) { - fullText.append("\n\n"); - } - - // Добавляем хештеги, убеждаясь что они начинаются с # - String hashtagsText = hashtags.stream() - .map(tag -> tag.startsWith("#") ? tag : "#" + tag) - .collect(Collectors.joining(" ")); - - fullText.append(hashtagsText); - } - - return fullText.toString(); - } - /** * Проверяет, является ли ошибка истечением токена * LinkedIn возвращает HTTP 401 (Unauthorized) для истекших токенов @@ -369,4 +346,3 @@ public class LinkedInPostingService { } } } - diff --git a/src/main/java/kz/konturai/parser/service/SocialPostTextFormatter.java b/src/main/java/kz/konturai/parser/service/SocialPostTextFormatter.java new file mode 100644 index 0000000..3add2a7 --- /dev/null +++ b/src/main/java/kz/konturai/parser/service/SocialPostTextFormatter.java @@ -0,0 +1,82 @@ +package kz.konturai.parser.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +final class SocialPostTextFormatter { + + static final String CONTACTS_BLOCK = "telegram - @konturaibot\nномер для звонка - 87273901485"; + + private static final String ELLIPSIS = "..."; + private static final String BLOCK_SEPARATOR = "\n\n"; + + private SocialPostTextFormatter() { + } + + static String buildPostText(String postText, List hashtags) { + return buildPostText(postText, hashtags, 0); + } + + static String buildPostText(String postText, List hashtags, int maxLength) { + String safePostText = postText != null ? postText : ""; + String hashtagsText = buildHashtagsText(hashtags); + String fullText = joinBlocks(safePostText, CONTACTS_BLOCK, hashtagsText); + + if (maxLength <= 0 || fullText.length() <= maxLength) { + return fullText; + } + + return buildPostTextWithinLimit(safePostText, hashtagsText, maxLength); + } + + private static String buildPostTextWithinLimit(String postText, String hashtagsText, int maxLength) { + String requiredSuffix = joinBlocks(CONTACTS_BLOCK, hashtagsText); + if (requiredSuffix.length() >= maxLength) { + return requiredSuffix.substring(0, maxLength); + } + + if (postText.isEmpty()) { + return requiredSuffix; + } + + int availablePostLength = maxLength - requiredSuffix.length() - BLOCK_SEPARATOR.length(); + if (availablePostLength <= 0) { + return requiredSuffix; + } + + return truncateWithEllipsis(postText, availablePostLength) + BLOCK_SEPARATOR + requiredSuffix; + } + + private static String buildHashtagsText(List hashtags) { + if (hashtags == null || hashtags.isEmpty()) { + return ""; + } + + return hashtags.stream() + .filter(tag -> tag != null && !tag.isBlank()) + .map(String::strip) + .map(tag -> tag.startsWith("#") ? tag : "#" + tag) + .collect(Collectors.joining(" ")); + } + + private static String joinBlocks(String... blocks) { + List filledBlocks = new ArrayList<>(); + for (String block : blocks) { + if (block != null && !block.isBlank()) { + filledBlocks.add(block); + } + } + return String.join(BLOCK_SEPARATOR, filledBlocks); + } + + private static String truncateWithEllipsis(String value, int maxLength) { + if (value.length() <= maxLength) { + return value; + } + if (maxLength <= ELLIPSIS.length()) { + return value.substring(0, maxLength); + } + return value.substring(0, maxLength - ELLIPSIS.length()) + ELLIPSIS; + } +} diff --git a/src/main/java/kz/konturai/parser/service/TelegramPostingService.java b/src/main/java/kz/konturai/parser/service/TelegramPostingService.java index 4be2571..d5eeea9 100644 --- a/src/main/java/kz/konturai/parser/service/TelegramPostingService.java +++ b/src/main/java/kz/konturai/parser/service/TelegramPostingService.java @@ -20,7 +20,6 @@ import reactor.netty.http.client.HttpClient; import java.time.Duration; import java.util.List; -import java.util.stream.Collectors; @Service public class TelegramPostingService { @@ -52,8 +51,8 @@ public class TelegramPostingService { // Парсим credentials TelegramCredentials creds = parseCredentials(credentials); - // Формируем полный текст поста с хештегами - String fullPostText = buildPostText(postText, hashtags); + // Формируем полный текст поста с контактами и хештегами + String fullPostText = SocialPostTextFormatter.buildPostText(postText, hashtags, 4096); // Проверяем длину сообщения (Telegram limit: 4096 characters) if (fullPostText.length() > 4096) { @@ -102,8 +101,8 @@ public class TelegramPostingService { // Парсим credentials TelegramCredentials creds = parseCredentials(credentials); - // Формируем полный текст поста с хештегами - String fullPostText = buildPostText(postText, hashtags); + // Формируем полный текст поста с контактами и хештегами + String fullPostText = SocialPostTextFormatter.buildPostText(postText, hashtags, 1024); // Проверяем длину сообщения (Telegram limit: 1024 characters для caption) if (fullPostText.length() > 1024) { @@ -256,28 +255,6 @@ public class TelegramPostingService { } } - /** - * Формирует полный текст поста с хештегами - */ - private String buildPostText(String postText, List hashtags) { - StringBuilder fullText = new StringBuilder(postText != null ? postText : ""); - - if (hashtags != null && !hashtags.isEmpty()) { - if (fullText.length() > 0) { - fullText.append("\n\n"); - } - - // Добавляем хештеги, убеждаясь что они начинаются с # - String hashtagsText = hashtags.stream() - .map(tag -> tag.startsWith("#") ? tag : "#" + tag) - .collect(Collectors.joining(" ")); - - fullText.append(hashtagsText); - } - - return fullText.toString(); - } - /** * Проверяет, является ли ошибка истечением токена * Telegram возвращает HTTP 401 (Unauthorized) для недействительных токенов @@ -357,4 +334,3 @@ public class TelegramPostingService { } } } - diff --git a/src/test/java/kz/konturai/parser/service/SocialPostTextFormatterTest.java b/src/test/java/kz/konturai/parser/service/SocialPostTextFormatterTest.java new file mode 100644 index 0000000..5c71658 --- /dev/null +++ b/src/test/java/kz/konturai/parser/service/SocialPostTextFormatterTest.java @@ -0,0 +1,42 @@ +package kz.konturai.parser.service; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SocialPostTextFormatterTest { + + @Test + void buildPostTextShouldAlwaysAppendContactsBeforeHashtags() { + String text = SocialPostTextFormatter.buildPostText("Новый продукт", List.of("ai", "#crm")); + + assertEquals( + "Новый продукт\n\n" + + "telegram - @konturaibot\n" + + "номер для звонка - 87273901485\n\n" + + "#ai #crm", + text); + } + + @Test + void buildPostTextShouldIncludeContactsWhenPostTextAndHashtagsAreMissing() { + String text = SocialPostTextFormatter.buildPostText(null, List.of()); + + assertEquals(SocialPostTextFormatter.CONTACTS_BLOCK, text); + } + + @Test + void buildPostTextShouldPreserveContactsWithinTelegramLimit() { + String longPostText = "A".repeat(1_100); + + String text = SocialPostTextFormatter.buildPostText(longPostText, List.of("marketing"), 120); + + assertTrue(text.length() <= 120); + assertTrue(text.contains("telegram - @konturaibot")); + assertTrue(text.contains("номер для звонка - 87273901485")); + assertTrue(text.endsWith("#marketing")); + } +}