This commit is contained in:
Magzhan Zhumabayev
2026-05-12 19:19:24 +05:00
parent 488a61d852
commit b41ce6293e
5 changed files with 136 additions and 72 deletions
@@ -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<String> hashtags) {
validateToken(creds);
try {
String fullText = buildPostText(postText, hashtags);
String fullText = SocialPostTextFormatter.buildPostText(postText, hashtags);
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("access_token", creds.pageToken);
@@ -124,7 +123,7 @@ public class FacebookPostingService {
private String postWithPhoto(PageCredentials creds, String postText, List<String> hashtags, byte[] imageData) {
validateToken(creds);
try {
String fullText = buildPostText(postText, hashtags);
String fullText = SocialPostTextFormatter.buildPostText(postText, hashtags);
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("access_token", creds.pageToken);
@@ -157,7 +156,7 @@ public class FacebookPostingService {
private String postWithVideo(PageCredentials creds, String postText, List<String> 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<String> 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;
}
}
}
}
@@ -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<String> 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<String> 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<String> 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 {
}
}
}
@@ -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<String> hashtags) {
return buildPostText(postText, hashtags, 0);
}
static String buildPostText(String postText, List<String> 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<String> 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<String> 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;
}
}
@@ -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<String> 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 {
}
}
}
@@ -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"));
}
}