From a32316e5177077029885c24ace6d2e8cc392b067 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Mon, 8 Dec 2025 11:01:08 +0100 Subject: [PATCH 01/12] Add Telegram bot integration for pipeline processing Introduced Telegram bot support by adding configuration properties, updating settings template, and including the TelegramBots library. Implemented TelegramPipelineBot to handle file uploads via Telegram and process them through the pipeline, with configurable timeouts and polling intervals. --- .../common/model/ApplicationProperties.java | 11 + app/core/build.gradle | 1 + .../SPDF/config/TelegramBotConfig.java | 18 ++ .../service/telegram/TelegramPipelineBot.java | 293 ++++++++++++++++++ .../src/main/resources/settings.yml.template | 8 + 5 files changed, 331 insertions(+) create mode 100644 app/core/src/main/java/stirling/software/SPDF/config/TelegramBotConfig.java create mode 100644 app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index f6afa62ea..efe148d3b 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -60,6 +60,7 @@ public class ApplicationProperties { private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated(); private Mail mail = new Mail(); + private Telegram telegram = new Telegram(); private Premium premium = new Premium(); @@ -576,6 +577,16 @@ public class ApplicationProperties { private String from; } + @Data + public static class Telegram { + private Boolean enabled = false; + private String botToken; + private String botUsername; + private String pipelineInboxFolder = "telegram"; + private long processingTimeoutSeconds = 180; + private long pollingIntervalMillis = 2000; + } + @Data public static class Premium { private boolean enabled; diff --git a/app/core/build.gradle b/app/core/build.gradle index 0ff725d3d..8fed275fc 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -60,6 +60,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-jetty' implementation 'com.posthog.java:posthog:1.2.0' implementation 'commons-io:commons-io:2.20.0' + implementation 'org.telegram:telegrambots:6.9.7.1' implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" implementation 'io.micrometer:micrometer-core:1.15.4' diff --git a/app/core/src/main/java/stirling/software/SPDF/config/TelegramBotConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/TelegramBotConfig.java new file mode 100644 index 000000000..92f126ed5 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/config/TelegramBotConfig.java @@ -0,0 +1,18 @@ +package stirling.software.SPDF.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.telegram.telegrambots.meta.TelegramBotsApi; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; +import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; + +@Configuration +@ConditionalOnProperty(prefix = "telegram", name = "enabled", havingValue = "true") +public class TelegramBotConfig { + + @Bean + public TelegramBotsApi telegramBotsApi() throws TelegramApiException { + return new TelegramBotsApi(DefaultBotSession.class); + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java new file mode 100644 index 000000000..57c83f372 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -0,0 +1,293 @@ +package stirling.software.SPDF.service.telegram; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Stream; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.bots.TelegramLongPollingBot; +import org.telegram.telegrambots.meta.TelegramBotsApi; +import org.telegram.telegrambots.meta.api.methods.GetFile; +import org.telegram.telegrambots.meta.api.methods.send.SendDocument; +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.objects.Chat; +import org.telegram.telegrambots.meta.api.objects.Document; +import org.telegram.telegrambots.meta.api.objects.File; +import org.telegram.telegrambots.meta.api.objects.InputFile; +import org.telegram.telegrambots.meta.api.objects.Message; +import org.telegram.telegrambots.meta.api.objects.PhotoSize; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + +import jakarta.annotation.PostConstruct; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.configuration.RuntimePathConfig; +import stirling.software.common.model.ApplicationProperties; + +@Slf4j +@Component +@ConditionalOnProperty(prefix = "telegram", name = "enabled", havingValue = "true") +public class TelegramPipelineBot extends TelegramLongPollingBot { + + private final ApplicationProperties.Telegram telegramProperties; + private final RuntimePathConfig runtimePathConfig; + private final TelegramBotsApi telegramBotsApi; + + public TelegramPipelineBot( + ApplicationProperties applicationProperties, + RuntimePathConfig runtimePathConfig, + TelegramBotsApi telegramBotsApi) { + super(applicationProperties.getTelegram().getBotToken()); + this.telegramProperties = applicationProperties.getTelegram(); + this.runtimePathConfig = runtimePathConfig; + this.telegramBotsApi = telegramBotsApi; + } + + @PostConstruct + public void register() { + if (StringUtils.isAnyBlank(getBotUsername(), getBotToken())) { + log.warn("Telegram bot disabled because botToken or botUsername is not configured"); + return; + } + try { + telegramBotsApi.registerBot(this); + log.info("Telegram pipeline bot registered as {}", getBotUsername()); + } catch (TelegramApiException e) { + log.error("Failed to register Telegram bot", e); + } + } + + @Override + public void onUpdateReceived(Update update) { + Message message = null; + + if (update.hasMessage()) { + message = update.getMessage(); + } + // 2) Channel posts + else if (update.hasChannelPost()) { + message = update.getChannelPost(); + } else { + return; + } + + Chat chat = message.getChat(); + if (chat == null) { + return; + } + + String chatType = chat.getType(); + if (!Objects.equals(chatType, "private") + && !Objects.equals(chatType, "group") + && !Objects.equals(chatType, "supergroup") + && !Objects.equals(chatType, "channel")) { + return; + } + + log.info( + "Received message {} in chat {} (type={}) message {}", + message.getMessageId(), + chat.getId(), + chatType, + message.getCaption()); + + if (message.hasDocument() || message.hasPhoto()) { + handleIncomingFile(message); + } + } + + @Override + public String getBotUsername() { + return telegramProperties.getBotUsername(); + } + + @Override + public String getBotToken() { + return telegramProperties.getBotToken(); + } + + private void handleIncomingFile(Message message) { + Long chatId = message.getChatId(); + String chatType = message.getChat() != null ? message.getChat().getType() : null; + + try { + // Only send status messages in private chats and groups, not in channels + if (!Objects.equals(chatType, "channel")) { + sendMessage(chatId, "File received. Starting processing in pipeline folder..."); + } + + PipelineFileInfo fileInfo = downloadMessageFile(message); + List outputs = waitForPipelineOutputs(fileInfo); + + if (outputs.isEmpty()) { + sendMessage( + chatId, + "No results were found in the pipeline finished folder. Please check your" + + " pipeline configuration."); + return; + } + + for (Path output : outputs) { + SendDocument sendDocument = new SendDocument(); + sendDocument.setChatId(chatId); + sendDocument.setDocument( + new InputFile(output.toFile(), output.getFileName().toString())); + execute(sendDocument); + } + + } catch (TelegramApiException e) { + log.error("Telegram API error while processing message {}", message.getMessageId(), e); + sendMessage(chatId, "Error during processing: Telegram API error."); + } catch (IOException e) { + log.error("IO error while processing message {}", message.getMessageId(), e); + sendMessage(chatId, "Error during processing: An IO error occurred."); + } catch (Exception e) { + log.error("Unexpected error while processing message {}", message.getMessageId(), e); + sendMessage(chatId, "Error during processing: An unexpected error occurred."); + } + } + + private PipelineFileInfo downloadMessageFile(Message message) + throws TelegramApiException, IOException { + if (message.hasDocument()) { + return downloadDocument(message.getDocument()); + } + if (message.hasPhoto()) { + PhotoSize photo = + message.getPhoto().stream() + .max(Comparator.comparing(PhotoSize::getFileSize)) + .orElseThrow( + () -> new IllegalStateException("Photo could not be loaded")); + return downloadFile(photo.getFileId(), "photo-" + message.getMessageId() + ".jpg"); + } + throw new IllegalArgumentException("Unsupported file type"); + } + + private PipelineFileInfo downloadDocument(Document document) + throws TelegramApiException, IOException { + String filename = document.getFileName(); + String name = + StringUtils.isNotBlank(filename) ? filename : document.getFileUniqueId() + ".bin"; + return downloadFile(document.getFileId(), name); + } + + private PipelineFileInfo downloadFile(String fileId, String originalName) + throws TelegramApiException, IOException { + + GetFile getFile = new GetFile(fileId); + File telegramFile = execute(getFile); + + if (telegramFile == null || StringUtils.isBlank(telegramFile.getFilePath())) { + throw new IOException("Telegram did not return a valid file path"); + } + + URL downloadUrl = buildDownloadUrl(telegramFile.getFilePath()); + + Path targetDir = + Paths.get( + runtimePathConfig.getPipelineWatchedFoldersPath(), + telegramProperties.getPipelineInboxFolder()); + Files.createDirectories(targetDir); + + String uniqueBaseName = FilenameUtils.getBaseName(originalName) + "-" + UUID.randomUUID(); + String extension = FilenameUtils.getExtension(originalName); + String targetFilename = + extension.isBlank() ? uniqueBaseName : uniqueBaseName + "." + extension; + Path targetFile = targetDir.resolve(targetFilename); + + try (InputStream inputStream = downloadUrl.openStream()) { + Files.copy(inputStream, targetFile); + } + + log.info("Saved Telegram file {} to {}", originalName, targetFile); + return new PipelineFileInfo(targetFile, uniqueBaseName, Instant.now()); + } + + private URL buildDownloadUrl(String filePath) throws MalformedURLException { + return new URL( + String.format("https://api.telegram.org/file/bot%s/%s", getBotToken(), filePath)); + } + + private List waitForPipelineOutputs(PipelineFileInfo info) throws IOException { + + Path finishedDir = Paths.get(runtimePathConfig.getPipelineFinishedFoldersPath()); + Files.createDirectories(finishedDir); + + Instant start = info.savedAt(); + Duration timeout = Duration.ofSeconds(telegramProperties.getProcessingTimeoutSeconds()); + Duration pollInterval = Duration.ofMillis(telegramProperties.getPollingIntervalMillis()); + List foundOutputs = new ArrayList<>(); + + while (Duration.between(start, Instant.now()).compareTo(timeout) <= 0) { + try (Stream files = Files.walk(finishedDir, 1)) { + foundOutputs = + files.filter(Files::isRegularFile) + .filter(path -> matchesBaseName(info.uniqueBaseName(), path)) + .filter(path -> isNewerThan(path, start)) + .sorted(Comparator.comparing(Path::toString)) + .toList(); + } + + if (!foundOutputs.isEmpty()) { + break; + } + + try { + Thread.sleep(pollInterval.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + + return foundOutputs; + } + + private boolean matchesBaseName(String baseName, Path path) { + return path.getFileName().toString().contains(baseName); + } + + private boolean isNewerThan(Path path, Instant instant) { + try { + FileTime modifiedTime = Files.getLastModifiedTime(path); + return modifiedTime.toInstant().isAfter(instant); + } catch (IOException e) { + log.debug("Could not read modification time for {}", path, e); + return false; + } + } + + private void sendMessage(Long chatId, String text) { + if (chatId == null) { + return; + } + SendMessage message = new SendMessage(); + message.setChatId(chatId); + message.setText(text); + try { + execute(message); + } catch (TelegramApiException e) { + log.warn("Failed to send Telegram message to {}", chatId, e); + } + } + + private record PipelineFileInfo(Path originalFile, String uniqueBaseName, Instant savedAt) {} +} diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index 4c2ec003e..bdd6cca30 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -105,6 +105,14 @@ mail: password: '' # SMTP server password from: '' # sender email address +telegram: + enabled: false # set to 'true' to enable Telegram bot integration + botToken: '' # Telegram bot token obtained from BotFather + botUsername: '' # Telegram bot username (without @) + pipelineInboxFolder: telegram # Name of the pipeline inbox folder for Telegram uploads + processingTimeoutSeconds: 180 # Maximum time in seconds to wait for processing a Telegram request + pollingIntervalMillis: 2000 # Interval in milliseconds between polling for new messages + legal: termsAndConditions: https://www.stirling.com/legal/terms-of-service # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder privacyPolicy: https://www.stirling.com/legal/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder From fa653ea4cf968ef29fac88d4d4dd091c5b4e8d82 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Tue, 9 Dec 2025 09:19:14 +0100 Subject: [PATCH 02/12] Add MIME type check and improve file handling in Telegram bot Introduces a MIME type check for uploaded documents, allowing only PDFs. Refactors file download methods to pass the full message object, enabling use of captions as inbox folder names. Replaces Thread.sleep with synchronized wait for polling, and improves handling of file naming and directory creation. --- .../service/telegram/TelegramPipelineBot.java | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index 57c83f372..8beda2c76 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -107,7 +107,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { message.getMessageId(), chat.getId(), chatType, - message.getCaption()); + message); if (message.hasDocument() || message.hasPhoto()) { handleIncomingFile(message); @@ -127,7 +127,18 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { private void handleIncomingFile(Message message) { Long chatId = message.getChatId(); String chatType = message.getChat() != null ? message.getChat().getType() : null; - + String[] allowedMimeTypes = {"application/pdf"}; + Document document = message.getDocument(); + if (document != null) { + String mimeType = document.getMimeType(); + if (mimeType != null && !List.of(allowedMimeTypes).contains(mimeType.toLowerCase())) { + sendMessage( + message.getChatId(), + String.format( + "File mime type %s is not allowed. Allowed types are: %s", + mimeType, String.join(", ", allowedMimeTypes))); + } + } try { // Only send status messages in private chats and groups, not in channels if (!Objects.equals(chatType, "channel")) { @@ -141,7 +152,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { sendMessage( chatId, "No results were found in the pipeline finished folder. Please check your" - + " pipeline configuration."); + + " pipeline configuration."); return; } @@ -168,7 +179,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { private PipelineFileInfo downloadMessageFile(Message message) throws TelegramApiException, IOException { if (message.hasDocument()) { - return downloadDocument(message.getDocument()); + return downloadDocument(message); } if (message.hasPhoto()) { PhotoSize photo = @@ -176,20 +187,32 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { .max(Comparator.comparing(PhotoSize::getFileSize)) .orElseThrow( () -> new IllegalStateException("Photo could not be loaded")); - return downloadFile(photo.getFileId(), "photo-" + message.getMessageId() + ".jpg"); + return downloadFile( + photo.getFileId(), + "photo-" + message.getMessageId() + ".jpg", + message.getCaption()); } throw new IllegalArgumentException("Unsupported file type"); } - private PipelineFileInfo downloadDocument(Document document) + private PipelineFileInfo downloadDocument(Message message) throws TelegramApiException, IOException { + Document document = message.getDocument(); String filename = document.getFileName(); String name = StringUtils.isNotBlank(filename) ? filename : document.getFileUniqueId() + ".bin"; - return downloadFile(document.getFileId(), name); + // Check file mime type and file size + // long maxFileSize = 100 * 1024 * 1024; // 100MB per file + // if (document.getFileSize() > maxFileSize) { + // throw new IOException( + // String.format( + // "File size %d exceeds maximum allowed size of %d bytes", + // document.getFileSize(), maxFileSize)); + // } + return downloadFile(document.getFileId(), name, null); } - private PipelineFileInfo downloadFile(String fileId, String originalName) + private PipelineFileInfo downloadFile(String fileId, String originalName, String caption) throws TelegramApiException, IOException { GetFile getFile = new GetFile(fileId); @@ -201,10 +224,12 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { URL downloadUrl = buildDownloadUrl(telegramFile.getFilePath()); - Path targetDir = - Paths.get( - runtimePathConfig.getPipelineWatchedFoldersPath(), - telegramProperties.getPipelineInboxFolder()); + String inboxFolder = + StringUtils.isNotBlank(caption) + ? caption.trim() + : telegramProperties.getPipelineInboxFolder(); + + Path targetDir = Paths.get(runtimePathConfig.getPipelineWatchedFoldersPath(), inboxFolder); Files.createDirectories(targetDir); String uniqueBaseName = FilenameUtils.getBaseName(originalName) + "-" + UUID.randomUUID(); @@ -250,11 +275,13 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { break; } - try { - Thread.sleep(pollInterval.toMillis()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; + synchronized (this) { + try { + wait(pollInterval.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } } } From fbdd640eef40d381b947e488ef8be68a3d573152 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Tue, 9 Dec 2025 09:22:35 +0100 Subject: [PATCH 03/12] Update TelegramPipelineBot.java --- .../software/SPDF/service/telegram/TelegramPipelineBot.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index 8beda2c76..99277c072 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -137,6 +137,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { String.format( "File mime type %s is not allowed. Allowed types are: %s", mimeType, String.join(", ", allowedMimeTypes))); + return; } } try { From 0bca358838f918a0bd92e1d9049ea74fa341d137 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Tue, 9 Dec 2025 10:40:50 +0100 Subject: [PATCH 04/12] Add user and channel access controls to Telegram bot Introduces configuration options to restrict Telegram bot access to specific user and channel IDs. Updates ApplicationProperties, TelegramPipelineBot, and settings.yml.template to support allow lists and enable/disable flags for user and channel access control. --- .../common/model/ApplicationProperties.java | 6 ++ .../service/telegram/TelegramPipelineBot.java | 96 ++++++++++++++++++- .../src/main/resources/settings.yml.template | 5 + 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index efe148d3b..ad04ff4b6 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -548,6 +548,7 @@ public class ApplicationProperties { private boolean ssoAutoLogin; private CustomMetadata customMetadata = new CustomMetadata(); + @Deprecated @Data public static class CustomMetadata { private boolean autoUpdateMetadata; @@ -583,6 +584,11 @@ public class ApplicationProperties { private String botToken; private String botUsername; private String pipelineInboxFolder = "telegram"; + private Boolean customFolderSubfix = false; + private Boolean enableAllowUserIDs = false; + private List allowUserIDs = new ArrayList<>(); + private Boolean enableAllowChannelIDs = false; + private List allowChannelIDs = new ArrayList<>(); private long processingTimeoutSeconds = 180; private long pollingIntervalMillis = 2000; } diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index 99277c072..d5030bbce 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.service.telegram; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -33,6 +34,7 @@ import org.telegram.telegrambots.meta.api.objects.InputFile; import org.telegram.telegrambots.meta.api.objects.Message; import org.telegram.telegrambots.meta.api.objects.PhotoSize; import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; import jakarta.annotation.PostConstruct; @@ -79,6 +81,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { public void onUpdateReceived(Update update) { Message message = null; + // 1) Regular messages if (update.hasMessage()) { message = update.getMessage(); } @@ -109,6 +112,93 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { chatType, message); + User from = message.getFrom(); + if (telegramProperties.getEnableAllowUserIDs() + || telegramProperties.getEnableAllowChannelIDs()) { + List allowUserIDs = telegramProperties.getAllowUserIDs(); + List allowChannelIDs = telegramProperties.getAllowChannelIDs(); + switch (chatType) { + case "channel" -> { + // In private chats, messages are sent by users + if (telegramProperties.getEnableAllowChannelIDs()) { + if (allowChannelIDs.isEmpty()) { + log.info( + "No allowed channel IDs configured, disabling all access from" + + " private chat id={}", + chat.getId()); + } + if (from == null || !allowChannelIDs.contains(from.getId().longValue())) { + log.info( + "Ignoring message {} from user id={} in private chat id={} due" + + " to channel access restrictions", + message.getMessageId(), + from != null ? from.getId() : "unknown", + chat.getId()); + sendMessage( + chat.getId(), + "You are not authorized to use this bot. Please contact the" + + " administrator."); + return; + } + } + } + case "private" -> { + // In channels, messages are always sent on behalf of the channel + if (telegramProperties.getEnableAllowUserIDs()) { + if (allowUserIDs.isEmpty()) { + log.info( + "No allowed user IDs configured, disabling all access from" + + " channel id={}", + chat.getId()); + } + if (from == null || !allowUserIDs.contains(from.getId().longValue())) { + log.info( + "Ignoring message {} from channel id={} due to user access" + + " restrictions", + message.getMessageId(), + chat.getId()); + sendMessage( + chat.getId(), + "This channel is not authorized to use this bot. Please contact" + + " the administrator."); + return; + } + } + } + case "group", "supergroup" -> { + // group chats + } + default -> { + // private chats + } + } + boolean userAllowed = + !telegramProperties.getEnableAllowUserIDs() + || (from != null && allowUserIDs.contains(from.getId())); + boolean channelAllowed = + !telegramProperties.getEnableAllowChannelIDs() + || allowChannelIDs.contains(chat.getId()); + if (!userAllowed && !channelAllowed) { + log.info( + "Ignoring message {} from user id={} in chat id={} due to access restrictions", + message.getMessageId(), + from != null ? from.getId() : "unknown", + chat.getId()); + sendMessage( + chat.getId(), + "You are not authorized to use this bot. Please contact the administrator."); + return; + } + } + if (from != null) { + log.info( + "Message {} sent by user {} {} (id={})", + message.getMessageId(), + from.getFirstName(), + from.getLastName() != null ? from.getLastName() : "", + from.getId()); + } + if (message.hasDocument() || message.hasPhoto()) { handleIncomingFile(message); } @@ -248,8 +338,10 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { } private URL buildDownloadUrl(String filePath) throws MalformedURLException { - return new URL( - String.format("https://api.telegram.org/file/bot%s/%s", getBotToken(), filePath)); + return URI.create( + String.format( + "https://api.telegram.org/file/bot%s/%s", getBotToken(), filePath)) + .toURL(); } private List waitForPipelineOutputs(PipelineFileInfo info) throws IOException { diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index bdd6cca30..fd1ad9754 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -110,6 +110,11 @@ telegram: botToken: '' # Telegram bot token obtained from BotFather botUsername: '' # Telegram bot username (without @) pipelineInboxFolder: telegram # Name of the pipeline inbox folder for Telegram uploads + customFolderSubfix: true # set to 'true' to allow users to specify custom target folders via UserID + enableAllowUserIDs: true # set to 'true' to restrict access to specific Telegram user IDs + allowUserIDs: [] # List of allowed Telegram user IDs (e.g. [123456789, 987654321]). Leave empty to allow all users. + enableAllowChannelIDs: true # set to 'true' to restrict access to specific Telegram channel IDs + allowChannelIDs: [] # List of allowed Telegram channel IDs (e.g. [-1001234567890, -1009876543210]). Leave empty to allow all channels. processingTimeoutSeconds: 180 # Maximum time in seconds to wait for processing a Telegram request pollingIntervalMillis: 2000 # Interval in milliseconds between polling for new messages From 115d81a049549815f047aa09924ddfb82abe5eac Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Tue, 9 Dec 2025 10:47:38 +0100 Subject: [PATCH 05/12] Fix typo: customFolderSubfix to customFolderSuffix Corrects a typo in both ApplicationProperties.java and settings.yml.template, renaming 'customFolderSubfix' to 'customFolderSuffix' for consistency and clarity. --- .../stirling/software/common/model/ApplicationProperties.java | 2 +- app/core/src/main/resources/settings.yml.template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index ad04ff4b6..6c7a7f673 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -584,7 +584,7 @@ public class ApplicationProperties { private String botToken; private String botUsername; private String pipelineInboxFolder = "telegram"; - private Boolean customFolderSubfix = false; + private Boolean customFolderSuffix = false; private Boolean enableAllowUserIDs = false; private List allowUserIDs = new ArrayList<>(); private Boolean enableAllowChannelIDs = false; diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index fd1ad9754..58de2394f 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -110,7 +110,7 @@ telegram: botToken: '' # Telegram bot token obtained from BotFather botUsername: '' # Telegram bot username (without @) pipelineInboxFolder: telegram # Name of the pipeline inbox folder for Telegram uploads - customFolderSubfix: true # set to 'true' to allow users to specify custom target folders via UserID + customFolderSuffix: true # set to 'true' to allow users to specify custom target folders via UserID enableAllowUserIDs: true # set to 'true' to restrict access to specific Telegram user IDs allowUserIDs: [] # List of allowed Telegram user IDs (e.g. [123456789, 987654321]). Leave empty to allow all users. enableAllowChannelIDs: true # set to 'true' to restrict access to specific Telegram channel IDs From 8e50deea84e5ca4129b78e360b4bb01e6c87855f Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Tue, 9 Dec 2025 10:57:34 +0100 Subject: [PATCH 06/12] Remove redundant user and channel access checks Eliminated duplicate access restriction logic for user and channel IDs in TelegramPipelineBot. The code now relies on the earlier user ID check, simplifying message handling and reducing unnecessary authorization checks. --- .../service/telegram/TelegramPipelineBot.java | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index d5030bbce..94b5500a7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -151,7 +151,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { + " channel id={}", chat.getId()); } - if (from == null || !allowUserIDs.contains(from.getId().longValue())) { + if (from == null || !allowUserIDs.contains(from.getId())) { log.info( "Ignoring message {} from channel id={} due to user access" + " restrictions", @@ -172,23 +172,6 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { // private chats } } - boolean userAllowed = - !telegramProperties.getEnableAllowUserIDs() - || (from != null && allowUserIDs.contains(from.getId())); - boolean channelAllowed = - !telegramProperties.getEnableAllowChannelIDs() - || allowChannelIDs.contains(chat.getId()); - if (!userAllowed && !channelAllowed) { - log.info( - "Ignoring message {} from user id={} in chat id={} due to access restrictions", - message.getMessageId(), - from != null ? from.getId() : "unknown", - chat.getId()); - sendMessage( - chat.getId(), - "You are not authorized to use this bot. Please contact the administrator."); - return; - } } if (from != null) { log.info( From 5ed87ba1a1cebaa328d9c166129735580174be97 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Wed, 10 Dec 2025 10:27:17 +0100 Subject: [PATCH 07/12] Improve chat type and access control handling in bot Refactored TelegramPipelineBot to better distinguish between channel and private chat message handling, using senderChat for channels and from for private chats. Improved logging for unauthorized access and empty allow lists, and clarified log messages for unsupported chat types. --- .../service/telegram/TelegramPipelineBot.java | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index 94b5500a7..50ce3e3d2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -102,6 +102,11 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { && !Objects.equals(chatType, "group") && !Objects.equals(chatType, "supergroup") && !Objects.equals(chatType, "channel")) { + log.debug( + "Ignoring message {} in chat {} with unsupported chat type {}", + message.getMessageId(), + chat.getId(), + chatType); return; } @@ -112,50 +117,22 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { chatType, message); - User from = message.getFrom(); if (telegramProperties.getEnableAllowUserIDs() || telegramProperties.getEnableAllowChannelIDs()) { List allowUserIDs = telegramProperties.getAllowUserIDs(); List allowChannelIDs = telegramProperties.getAllowChannelIDs(); switch (chatType) { case "channel" -> { - // In private chats, messages are sent by users + // In channels, messages are always sent on behalf of the channel if (telegramProperties.getEnableAllowChannelIDs()) { - if (allowChannelIDs.isEmpty()) { - log.info( - "No allowed channel IDs configured, disabling all access from" - + " private chat id={}", - chat.getId()); - } - if (from == null || !allowChannelIDs.contains(from.getId().longValue())) { + Chat senderChat = message.getSenderChat(); + if ((senderChat == null || !allowChannelIDs.contains(senderChat.getId())) + && !allowChannelIDs.isEmpty()) { log.info( "Ignoring message {} from user id={} in private chat id={} due" + " to channel access restrictions", message.getMessageId(), - from != null ? from.getId() : "unknown", - chat.getId()); - sendMessage( - chat.getId(), - "You are not authorized to use this bot. Please contact the" - + " administrator."); - return; - } - } - } - case "private" -> { - // In channels, messages are always sent on behalf of the channel - if (telegramProperties.getEnableAllowUserIDs()) { - if (allowUserIDs.isEmpty()) { - log.info( - "No allowed user IDs configured, disabling all access from" - + " channel id={}", - chat.getId()); - } - if (from == null || !allowUserIDs.contains(from.getId())) { - log.info( - "Ignoring message {} from channel id={} due to user access" - + " restrictions", - message.getMessageId(), + senderChat != null ? senderChat.getId() : "unknown", chat.getId()); sendMessage( chat.getId(), @@ -163,24 +140,49 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { + " the administrator."); return; } + if (allowChannelIDs.isEmpty()) { + // All channels are allowed, but log a warning + log.warn( + "No allowed channel IDs configured, allowing all channels access. Channel with id={} sent a message in chat id={}", + senderChat != null ? senderChat.getId() : "unknown", + chat.getId()); + } + } + } + case "private" -> { + // In private chats, messages are sent by users + if (telegramProperties.getEnableAllowUserIDs()) { + User from = message.getFrom(); + if ((from == null || !allowUserIDs.contains(from.getId())) + && !allowUserIDs.isEmpty()) { + log.info( + "Ignoring message {} from channel id={} due to user access" + + " restrictions", + message.getMessageId(), + chat.getId()); + sendMessage( + chat.getId(), + "You are not authorized to use this bot. Please contact the" + + " administrator."); + return; + } + if (allowUserIDs.isEmpty()) { + // All users are allowed, but log a warning + log.warn( + "No allowed user IDs configured, allowing all users access. User with id={} sent a message in private chat id={}", + from != null ? from.getId() : "unknown", + chat.getId()); + } } } case "group", "supergroup" -> { // group chats } default -> { - // private chats + // should not reach here due to earlier chatType check } } } - if (from != null) { - log.info( - "Message {} sent by user {} {} (id={})", - message.getMessageId(), - from.getFirstName(), - from.getLastName() != null ? from.getLastName() : "", - from.getId()); - } if (message.hasDocument() || message.hasPhoto()) { handleIncomingFile(message); From 74203bc45b3da872eb7599a9fd8f8b4b6d37ad5e Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Wed, 10 Dec 2025 11:30:23 +0100 Subject: [PATCH 08/12] Refactor file handling to support only documents Removed support for photo uploads and now only documents are processed. File saving logic was updated to organize files into subfolders based on chat ID, improving file management and separation. Logging messages were also reformatted for better readability. --- .../service/telegram/TelegramPipelineBot.java | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index 50ce3e3d2..4792d489a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; @@ -32,7 +33,6 @@ import org.telegram.telegrambots.meta.api.objects.Document; import org.telegram.telegrambots.meta.api.objects.File; import org.telegram.telegrambots.meta.api.objects.InputFile; import org.telegram.telegrambots.meta.api.objects.Message; -import org.telegram.telegrambots.meta.api.objects.PhotoSize; import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; @@ -143,7 +143,9 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { if (allowChannelIDs.isEmpty()) { // All channels are allowed, but log a warning log.warn( - "No allowed channel IDs configured, allowing all channels access. Channel with id={} sent a message in chat id={}", + "No allowed channel IDs configured, allowing all channels" + + " access. Channel with id={} sent a message in chat" + + " id={}", senderChat != null ? senderChat.getId() : "unknown", chat.getId()); } @@ -169,7 +171,8 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { if (allowUserIDs.isEmpty()) { // All users are allowed, but log a warning log.warn( - "No allowed user IDs configured, allowing all users access. User with id={} sent a message in private chat id={}", + "No allowed user IDs configured, allowing all users access." + + " User with id={} sent a message in private chat id={}", from != null ? from.getId() : "unknown", chat.getId()); } @@ -184,9 +187,13 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { } } - if (message.hasDocument() || message.hasPhoto()) { + if (message.hasDocument()) { handleIncomingFile(message); + return; } + sendMessage( + chat.getId(), + "No valid file found in the message. Please send a document to process."); } @Override @@ -257,17 +264,6 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { if (message.hasDocument()) { return downloadDocument(message); } - if (message.hasPhoto()) { - PhotoSize photo = - message.getPhoto().stream() - .max(Comparator.comparing(PhotoSize::getFileSize)) - .orElseThrow( - () -> new IllegalStateException("Photo could not be loaded")); - return downloadFile( - photo.getFileId(), - "photo-" + message.getMessageId() + ".jpg", - message.getCaption()); - } throw new IllegalArgumentException("Unsupported file type"); } @@ -277,18 +273,10 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { String filename = document.getFileName(); String name = StringUtils.isNotBlank(filename) ? filename : document.getFileUniqueId() + ".bin"; - // Check file mime type and file size - // long maxFileSize = 100 * 1024 * 1024; // 100MB per file - // if (document.getFileSize() > maxFileSize) { - // throw new IOException( - // String.format( - // "File size %d exceeds maximum allowed size of %d bytes", - // document.getFileSize(), maxFileSize)); - // } - return downloadFile(document.getFileId(), name, null); + return downloadFile(document.getFileId(), name, message); } - private PipelineFileInfo downloadFile(String fileId, String originalName, String caption) + private PipelineFileInfo downloadFile(String fileId, String originalName, Message message) throws TelegramApiException, IOException { GetFile getFile = new GetFile(fileId); @@ -300,19 +288,34 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { URL downloadUrl = buildDownloadUrl(telegramFile.getFilePath()); - String inboxFolder = - StringUtils.isNotBlank(caption) - ? caption.trim() - : telegramProperties.getPipelineInboxFolder(); + Chat chat = message.getChat(); + Long chatId = chat != null ? chat.getId() : null; - Path targetDir = Paths.get(runtimePathConfig.getPipelineWatchedFoldersPath(), inboxFolder); - Files.createDirectories(targetDir); + Path baseInbox = + Paths.get( + runtimePathConfig.getPipelineWatchedFoldersPath(), + telegramProperties.getPipelineInboxFolder()); + + Files.createDirectories(baseInbox); + + Path inboxFolder = + Optional.ofNullable(chatId) + .map(Object::toString) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .map(baseInbox::resolve) // Unterordner: baseInbox + "/" + chatId + .orElse(baseInbox); // fallback: nur der Basisordner + + // Jetzt den eigentlichen Zielordner (mit chatId oder ohne) anlegen + Files.createDirectories(inboxFolder); String uniqueBaseName = FilenameUtils.getBaseName(originalName) + "-" + UUID.randomUUID(); String extension = FilenameUtils.getExtension(originalName); + String targetFilename = extension.isBlank() ? uniqueBaseName : uniqueBaseName + "." + extension; - Path targetFile = targetDir.resolve(targetFilename); + + Path targetFile = inboxFolder.resolve(targetFilename); try (InputStream inputStream = downloadUrl.openStream()) { Files.copy(inputStream, targetFile); From ac14ec930026c4c66b5baf04d4d14f406d7293f4 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Wed, 10 Dec 2025 20:39:20 +0100 Subject: [PATCH 09/12] Refactor inbox folder logic and add JSON config check Simplifies chatId extraction and refactors inbox folder selection to use a custom folder suffix only if enabled. Adds a check for the presence of a JSON configuration file in the inbox folder and notifies the user if none is found. --- .../service/telegram/TelegramPipelineBot.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index 4792d489a..fe812d89f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -288,8 +288,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { URL downloadUrl = buildDownloadUrl(telegramFile.getFilePath()); - Chat chat = message.getChat(); - Long chatId = chat != null ? chat.getId() : null; + Long chatId = message.getChat() != null ? message.getChat().getId() : null; Path baseInbox = Paths.get( @@ -298,17 +297,22 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { Files.createDirectories(baseInbox); - Path inboxFolder = - Optional.ofNullable(chatId) - .map(Object::toString) - .map(String::trim) - .filter(StringUtils::isNotBlank) - .map(baseInbox::resolve) // Unterordner: baseInbox + "/" + chatId - .orElse(baseInbox); // fallback: nur der Basisordner + Path inboxFolder = baseInbox; + if (telegramProperties.getCustomFolderSuffix() && chatId != null) { + inboxFolder = baseInbox.resolve(chatId.toString()); + } - // Jetzt den eigentlichen Zielordner (mit chatId oder ohne) anlegen Files.createDirectories(inboxFolder); + boolean hasJsonConfig = Files.list(inboxFolder) + .filter(Files::isRegularFile) + .anyMatch(p -> p.toString().endsWith(".json")); + + if (!hasJsonConfig) { + log.info("No JSON configuration file found in inbox folder {}", inboxFolder); + sendMessage(chatId, "No JSON configuration file found in the inbox folder. Please contact the administrator."); + } + String uniqueBaseName = FilenameUtils.getBaseName(originalName) + "-" + UUID.randomUUID(); String extension = FilenameUtils.getExtension(originalName); From 8aa2d59583de981794567ffaa45eea4337b16f53 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Wed, 10 Dec 2025 21:00:56 +0100 Subject: [PATCH 10/12] Refactor JSON config check formatting in Telegram bot Reformatted the code that checks for JSON configuration files in the inbox folder for improved readability. No functional changes were made. --- .../SPDF/service/telegram/TelegramPipelineBot.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index fe812d89f..f12f3bd31 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; @@ -304,13 +303,16 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { Files.createDirectories(inboxFolder); - boolean hasJsonConfig = Files.list(inboxFolder) - .filter(Files::isRegularFile) - .anyMatch(p -> p.toString().endsWith(".json")); + boolean hasJsonConfig = + Files.list(inboxFolder) + .filter(Files::isRegularFile) + .anyMatch(p -> p.toString().endsWith(".json")); if (!hasJsonConfig) { log.info("No JSON configuration file found in inbox folder {}", inboxFolder); - sendMessage(chatId, "No JSON configuration file found in the inbox folder. Please contact the administrator."); + sendMessage( + chatId, + "No JSON configuration file found in the inbox folder. Please contact the administrator."); } String uniqueBaseName = FilenameUtils.getBaseName(originalName) + "-" + UUID.randomUUID(); From b584b290970e9e2efb9b6fac4699b6818134133b Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Thu, 11 Dec 2025 10:05:59 +0100 Subject: [PATCH 11/12] Refactor TelegramPipelineBot for clarity and modularity Refactored TelegramPipelineBot to modularize chat type handling, authorization, and file processing logic. Introduced helper methods for message extraction, chat type checks, and authorization, and improved error handling and logging. Simplified file download and pipeline polling, and improved code readability and maintainability. --- .../service/telegram/TelegramPipelineBot.java | 627 +++++++++--------- 1 file changed, 330 insertions(+), 297 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index f12f3bd31..cb93226bf 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -8,13 +8,11 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Objects; import java.util.UUID; import java.util.stream.Stream; @@ -48,6 +46,15 @@ import stirling.software.common.model.ApplicationProperties; @ConditionalOnProperty(prefix = "telegram", name = "enabled", havingValue = "true") public class TelegramPipelineBot extends TelegramLongPollingBot { + private static final String CHAT_PRIVATE = "private"; + private static final String CHAT_GROUP = "group"; + private static final String CHAT_SUPERGROUP = "supergroup"; + private static final String CHAT_CHANNEL = "channel"; + + private static final List ALLOWED_MIME_TYPES = List.of("application/pdf"); + + private final Object pipelinePollMonitor = new Object(); + private final ApplicationProperties.Telegram telegramProperties; private final RuntimePathConfig runtimePathConfig; private final TelegramBotsApi telegramBotsApi; @@ -56,6 +63,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { ApplicationProperties applicationProperties, RuntimePathConfig runtimePathConfig, TelegramBotsApi telegramBotsApi) { + super(applicationProperties.getTelegram().getBotToken()); this.telegramProperties = applicationProperties.getTelegram(); this.runtimePathConfig = runtimePathConfig; @@ -78,111 +86,35 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { @Override public void onUpdateReceived(Update update) { - Message message = null; - - // 1) Regular messages - if (update.hasMessage()) { - message = update.getMessage(); - } - // 2) Channel posts - else if (update.hasChannelPost()) { - message = update.getChannelPost(); - } else { + Message message = extractMessage(update); + if (message == null) { return; } Chat chat = message.getChat(); - if (chat == null) { - return; - } - - String chatType = chat.getType(); - if (!Objects.equals(chatType, "private") - && !Objects.equals(chatType, "group") - && !Objects.equals(chatType, "supergroup") - && !Objects.equals(chatType, "channel")) { + if (chat == null || !isSupportedChatType(chat.getType())) { log.debug( - "Ignoring message {} in chat {} with unsupported chat type {}", + "Ignoring message {}, unsupported chat type {}", message.getMessageId(), - chat.getId(), - chatType); + chat != null ? chat.getType() : "null"); return; } - log.info( - "Received message {} in chat {} (type={}) message {}", - message.getMessageId(), - chat.getId(), - chatType, - message); + if (!isAuthorized(message, chat)) { + return; + } - if (telegramProperties.getEnableAllowUserIDs() - || telegramProperties.getEnableAllowChannelIDs()) { - List allowUserIDs = telegramProperties.getAllowUserIDs(); - List allowChannelIDs = telegramProperties.getAllowChannelIDs(); - switch (chatType) { - case "channel" -> { - // In channels, messages are always sent on behalf of the channel - if (telegramProperties.getEnableAllowChannelIDs()) { - Chat senderChat = message.getSenderChat(); - if ((senderChat == null || !allowChannelIDs.contains(senderChat.getId())) - && !allowChannelIDs.isEmpty()) { - log.info( - "Ignoring message {} from user id={} in private chat id={} due" - + " to channel access restrictions", - message.getMessageId(), - senderChat != null ? senderChat.getId() : "unknown", - chat.getId()); - sendMessage( - chat.getId(), - "This channel is not authorized to use this bot. Please contact" - + " the administrator."); - return; - } - if (allowChannelIDs.isEmpty()) { - // All channels are allowed, but log a warning - log.warn( - "No allowed channel IDs configured, allowing all channels" - + " access. Channel with id={} sent a message in chat" - + " id={}", - senderChat != null ? senderChat.getId() : "unknown", - chat.getId()); - } - } - } - case "private" -> { - // In private chats, messages are sent by users - if (telegramProperties.getEnableAllowUserIDs()) { - User from = message.getFrom(); - if ((from == null || !allowUserIDs.contains(from.getId())) - && !allowUserIDs.isEmpty()) { - log.info( - "Ignoring message {} from channel id={} due to user access" - + " restrictions", - message.getMessageId(), - chat.getId()); - sendMessage( - chat.getId(), - "You are not authorized to use this bot. Please contact the" - + " administrator."); - return; - } - if (allowUserIDs.isEmpty()) { - // All users are allowed, but log a warning - log.warn( - "No allowed user IDs configured, allowing all users access." - + " User with id={} sent a message in private chat id={}", - from != null ? from.getId() : "unknown", - chat.getId()); - } - } - } - case "group", "supergroup" -> { - // group chats - } - default -> { - // should not reach here due to earlier chatType check - } + if (update.hasMessage() && update.getMessage().hasText()) { + String message_text = update.getMessage().getText(); + long chat_id = update.getMessage().getChatId(); + if (message_text.equals("/start")) { + sendMessage( + chat_id, + "Welcome to the SPDF Telegram Bot!\n\n" + + "To get started, please send me a PDF document that you would like to process." + + " Make sure the document is in PDF format.\n\n" + + "Once I receive your document, I'll begin processing it through the pipeline."); + return; } } @@ -190,11 +122,312 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { handleIncomingFile(message); return; } + sendMessage( chat.getId(), "No valid file found in the message. Please send a document to process."); } + // --------------------------- + // Message Extraction / Chat Type + // --------------------------- + + private Message extractMessage(Update update) { + if (update.hasMessage()) return update.getMessage(); + if (update.hasChannelPost()) return update.getChannelPost(); + return null; + } + + private boolean isSupportedChatType(String type) { + return CHAT_PRIVATE.equals(type) + || CHAT_GROUP.equals(type) + || CHAT_SUPERGROUP.equals(type) + || CHAT_CHANNEL.equals(type); + } + + // --------------------------- + // Authorization + // --------------------------- + + private boolean isAuthorized(Message message, Chat chat) { + if (!(telegramProperties.getEnableAllowUserIDs() + || telegramProperties.getEnableAllowChannelIDs())) { + return true; + } + + return switch (chat.getType()) { + case CHAT_CHANNEL -> checkChannelAccess(message, chat); + case CHAT_PRIVATE -> checkUserAccess(message, chat); + case CHAT_GROUP, CHAT_SUPERGROUP -> true; // groups allowed by default + default -> false; + }; + } + + private boolean checkUserAccess(Message message, Chat chat) { + if (!telegramProperties.getEnableAllowUserIDs()) return true; + + User from = message.getFrom(); + List allow = telegramProperties.getAllowUserIDs(); + + if (allow.isEmpty()) { + log.warn("No allowed user IDs configured - allowing all users."); + return true; + } + + if (from == null || !allow.contains(from.getId())) { + log.info( + "Rejecting user {} in private chat {}", + from != null ? from.getId() : "unknown", + chat.getId()); + sendMessage(chat.getId(), "You are not authorized to use this bot."); + return false; + } + + return true; + } + + private boolean checkChannelAccess(Message message, Chat chat) { + if (!telegramProperties.getEnableAllowChannelIDs()) return true; + + Chat senderChat = message.getSenderChat(); + List allow = telegramProperties.getAllowChannelIDs(); + + if (allow.isEmpty()) { + log.warn("No allowed channel IDs configured - allowing all channels."); + return true; + } + + if (senderChat == null || !allow.contains(senderChat.getId())) { + log.info( + "Rejecting channel {} in chat {}", + senderChat != null ? senderChat.getId() : "unknown", + chat.getId()); + + sendMessage(chat.getId(), "This channel is not authorized to use this bot."); + return false; + } + + return true; + } + + // --------------------------- + // File Handling + // --------------------------- + + private void handleIncomingFile(Message message) { + Long chatId = message.getChatId(); + Document doc = message.getDocument(); + + if (doc == null) { + sendMessage(chatId, "No document found."); + return; + } + + if (doc.getMimeType() != null + && !ALLOWED_MIME_TYPES.contains(doc.getMimeType().toLowerCase())) { + sendMessage( + chatId, + "Unsupported MIME type: " + + doc.getMimeType() + + "\nAllowed: " + + String.join(", ", ALLOWED_MIME_TYPES)); + return; + } + + if (!hasJsonConfig(chatId)) { + sendMessage( + chatId, + "No JSON configuration file found in the pipeline inbox folder. Please contact the administrator."); + return; + } + + try { + if (!CHAT_CHANNEL.equals(message.getChat().getType())) { + sendMessage(chatId, "File received. Starting processing..."); + } + + PipelineFileInfo info = downloadMessageFile(message); + List outputs = waitForPipelineOutputs(info); + + if (outputs.isEmpty()) { + sendMessage( + chatId, + "No results were found in the pipeline output folder. Check" + + " configuration."); + return; + } + + for (Path file : outputs) { + SendDocument out = new SendDocument(); + out.setChatId(chatId); + out.setDocument(new InputFile(file.toFile(), file.getFileName().toString())); + execute(out); + } + + } catch (TelegramApiException e) { + log.error("Telegram API error", e); + sendMessage(chatId, "Telegram API error occurred."); + } catch (IOException e) { + log.error("IO error", e); + sendMessage(chatId, "An IO error occurred."); + } catch (Exception e) { + log.error("Unexpected error", e); + sendMessage(chatId, "Unexpected error occurred."); + } + } + + private PipelineFileInfo downloadMessageFile(Message message) + throws TelegramApiException, IOException { + Document document = message.getDocument(); + String filename = document.getFileName(); + String name = + StringUtils.isNotBlank(filename) ? filename : document.getFileUniqueId() + ".bin"; + + return downloadFile(document.getFileId(), name, message); + } + + private PipelineFileInfo downloadFile(String fileId, String originalName, Message message) + throws TelegramApiException, IOException { + + Long chatId = message.getChatId(); + + Path inboxFolder = getInboxFolder(chatId); + + GetFile getFile = new GetFile(fileId); + File tgFile = execute(getFile); + + if (tgFile == null || StringUtils.isBlank(tgFile.getFilePath())) { + throw new IOException("Telegram did not return a file path."); + } + + URL url = buildDownloadUrl(tgFile.getFilePath()); + + String base = FilenameUtils.getBaseName(originalName) + "-" + UUID.randomUUID(); + String ext = FilenameUtils.getExtension(originalName); + String outFile = ext.isBlank() ? base : base + "." + ext; + + Path targetFile = inboxFolder.resolve(outFile); + + try (InputStream in = url.openStream()) { + Files.copy(in, targetFile); + } + + log.info("Saved Telegram file {} to {}", originalName, targetFile); + return new PipelineFileInfo(targetFile, base, Instant.now()); + } + + private URL buildDownloadUrl(String filePath) throws MalformedURLException { + return URI.create("https://api.telegram.org/file/bot" + getBotToken() + "/" + filePath) + .toURL(); + } + + // --------------------------- + // Inbox-Ordner & JSON-Check + // --------------------------- + + private Path getInboxFolder(Long chatId) throws IOException { + Path baseInbox = + Paths.get( + runtimePathConfig.getPipelineWatchedFoldersPath(), + telegramProperties.getPipelineInboxFolder()); + + Files.createDirectories(baseInbox); + + Path inboxFolder = + telegramProperties.getCustomFolderSuffix() + ? baseInbox.resolve(chatId.toString()) + : baseInbox; + + Files.createDirectories(inboxFolder); + + return inboxFolder; + } + + private boolean hasJsonConfig(Long chatId) { + try { + Path inboxFolder = getInboxFolder(chatId); + try (Stream s = Files.list(inboxFolder)) { + return s.anyMatch(p -> p.toString().endsWith(".json")); + } + } catch (IOException e) { + log.error("Failed to check JSON config for chat {}", chatId, e); + return false; + } + } + + // --------------------------- + // Pipeline polling + // --------------------------- + + private List waitForPipelineOutputs(PipelineFileInfo info) throws IOException { + + Path finishedDir = Paths.get(runtimePathConfig.getPipelineFinishedFoldersPath()); + Files.createDirectories(finishedDir); + + Instant start = info.savedAt(); + Duration timeout = Duration.ofSeconds(telegramProperties.getProcessingTimeoutSeconds()); + Duration poll = Duration.ofMillis(telegramProperties.getPollingIntervalMillis()); + List results = new ArrayList<>(); + + while (Duration.between(start, Instant.now()).compareTo(timeout) <= 0) { + try (Stream s = Files.walk(finishedDir, 1)) { + results = + s.filter(Files::isRegularFile) + .filter(path -> matchesBaseName(info.uniqueBaseName(), path)) + .filter(path -> isNewerThan(path, start)) + .sorted(Comparator.comparing(Path::toString)) + .toList(); + } + + if (!results.isEmpty()) { + break; + } + + synchronized (pipelinePollMonitor) { + try { + pipelinePollMonitor.wait(poll.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + return results; + } + + private boolean matchesBaseName(String base, Path file) { + return file.getFileName().toString().contains(base); + } + + private boolean isNewerThan(Path path, Instant since) { + try { + return Files.getLastModifiedTime(path).toInstant().isAfter(since); + } catch (IOException e) { + log.debug("Could not read modification time for {}", path); + return false; + } + } + + // --------------------------- + // Messaging + // --------------------------- + + private void sendMessage(Long chatId, String text) { + if (chatId == null) return; + + SendMessage msg = new SendMessage(); + msg.setChatId(chatId); + msg.setText(text); + try { + execute(msg); + } catch (TelegramApiException e) { + log.warn("Failed to send message to {}", chatId, e); + } + } + + private record PipelineFileInfo(Path originalFile, String uniqueBaseName, Instant savedAt) {} + @Override public String getBotUsername() { return telegramProperties.getBotUsername(); @@ -204,204 +437,4 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { public String getBotToken() { return telegramProperties.getBotToken(); } - - private void handleIncomingFile(Message message) { - Long chatId = message.getChatId(); - String chatType = message.getChat() != null ? message.getChat().getType() : null; - String[] allowedMimeTypes = {"application/pdf"}; - Document document = message.getDocument(); - if (document != null) { - String mimeType = document.getMimeType(); - if (mimeType != null && !List.of(allowedMimeTypes).contains(mimeType.toLowerCase())) { - sendMessage( - message.getChatId(), - String.format( - "File mime type %s is not allowed. Allowed types are: %s", - mimeType, String.join(", ", allowedMimeTypes))); - return; - } - } - try { - // Only send status messages in private chats and groups, not in channels - if (!Objects.equals(chatType, "channel")) { - sendMessage(chatId, "File received. Starting processing in pipeline folder..."); - } - - PipelineFileInfo fileInfo = downloadMessageFile(message); - List outputs = waitForPipelineOutputs(fileInfo); - - if (outputs.isEmpty()) { - sendMessage( - chatId, - "No results were found in the pipeline finished folder. Please check your" - + " pipeline configuration."); - return; - } - - for (Path output : outputs) { - SendDocument sendDocument = new SendDocument(); - sendDocument.setChatId(chatId); - sendDocument.setDocument( - new InputFile(output.toFile(), output.getFileName().toString())); - execute(sendDocument); - } - - } catch (TelegramApiException e) { - log.error("Telegram API error while processing message {}", message.getMessageId(), e); - sendMessage(chatId, "Error during processing: Telegram API error."); - } catch (IOException e) { - log.error("IO error while processing message {}", message.getMessageId(), e); - sendMessage(chatId, "Error during processing: An IO error occurred."); - } catch (Exception e) { - log.error("Unexpected error while processing message {}", message.getMessageId(), e); - sendMessage(chatId, "Error during processing: An unexpected error occurred."); - } - } - - private PipelineFileInfo downloadMessageFile(Message message) - throws TelegramApiException, IOException { - if (message.hasDocument()) { - return downloadDocument(message); - } - throw new IllegalArgumentException("Unsupported file type"); - } - - private PipelineFileInfo downloadDocument(Message message) - throws TelegramApiException, IOException { - Document document = message.getDocument(); - String filename = document.getFileName(); - String name = - StringUtils.isNotBlank(filename) ? filename : document.getFileUniqueId() + ".bin"; - return downloadFile(document.getFileId(), name, message); - } - - private PipelineFileInfo downloadFile(String fileId, String originalName, Message message) - throws TelegramApiException, IOException { - - GetFile getFile = new GetFile(fileId); - File telegramFile = execute(getFile); - - if (telegramFile == null || StringUtils.isBlank(telegramFile.getFilePath())) { - throw new IOException("Telegram did not return a valid file path"); - } - - URL downloadUrl = buildDownloadUrl(telegramFile.getFilePath()); - - Long chatId = message.getChat() != null ? message.getChat().getId() : null; - - Path baseInbox = - Paths.get( - runtimePathConfig.getPipelineWatchedFoldersPath(), - telegramProperties.getPipelineInboxFolder()); - - Files.createDirectories(baseInbox); - - Path inboxFolder = baseInbox; - if (telegramProperties.getCustomFolderSuffix() && chatId != null) { - inboxFolder = baseInbox.resolve(chatId.toString()); - } - - Files.createDirectories(inboxFolder); - - boolean hasJsonConfig = - Files.list(inboxFolder) - .filter(Files::isRegularFile) - .anyMatch(p -> p.toString().endsWith(".json")); - - if (!hasJsonConfig) { - log.info("No JSON configuration file found in inbox folder {}", inboxFolder); - sendMessage( - chatId, - "No JSON configuration file found in the inbox folder. Please contact the administrator."); - } - - String uniqueBaseName = FilenameUtils.getBaseName(originalName) + "-" + UUID.randomUUID(); - String extension = FilenameUtils.getExtension(originalName); - - String targetFilename = - extension.isBlank() ? uniqueBaseName : uniqueBaseName + "." + extension; - - Path targetFile = inboxFolder.resolve(targetFilename); - - try (InputStream inputStream = downloadUrl.openStream()) { - Files.copy(inputStream, targetFile); - } - - log.info("Saved Telegram file {} to {}", originalName, targetFile); - return new PipelineFileInfo(targetFile, uniqueBaseName, Instant.now()); - } - - private URL buildDownloadUrl(String filePath) throws MalformedURLException { - return URI.create( - String.format( - "https://api.telegram.org/file/bot%s/%s", getBotToken(), filePath)) - .toURL(); - } - - private List waitForPipelineOutputs(PipelineFileInfo info) throws IOException { - - Path finishedDir = Paths.get(runtimePathConfig.getPipelineFinishedFoldersPath()); - Files.createDirectories(finishedDir); - - Instant start = info.savedAt(); - Duration timeout = Duration.ofSeconds(telegramProperties.getProcessingTimeoutSeconds()); - Duration pollInterval = Duration.ofMillis(telegramProperties.getPollingIntervalMillis()); - List foundOutputs = new ArrayList<>(); - - while (Duration.between(start, Instant.now()).compareTo(timeout) <= 0) { - try (Stream files = Files.walk(finishedDir, 1)) { - foundOutputs = - files.filter(Files::isRegularFile) - .filter(path -> matchesBaseName(info.uniqueBaseName(), path)) - .filter(path -> isNewerThan(path, start)) - .sorted(Comparator.comparing(Path::toString)) - .toList(); - } - - if (!foundOutputs.isEmpty()) { - break; - } - - synchronized (this) { - try { - wait(pollInterval.toMillis()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } - } - - return foundOutputs; - } - - private boolean matchesBaseName(String baseName, Path path) { - return path.getFileName().toString().contains(baseName); - } - - private boolean isNewerThan(Path path, Instant instant) { - try { - FileTime modifiedTime = Files.getLastModifiedTime(path); - return modifiedTime.toInstant().isAfter(instant); - } catch (IOException e) { - log.debug("Could not read modification time for {}", path, e); - return false; - } - } - - private void sendMessage(Long chatId, String text) { - if (chatId == null) { - return; - } - SendMessage message = new SendMessage(); - message.setChatId(chatId); - message.setText(text); - try { - execute(message); - } catch (TelegramApiException e) { - log.warn("Failed to send Telegram message to {}", chatId, e); - } - } - - private record PipelineFileInfo(Path originalFile, String uniqueBaseName, Instant savedAt) {} } From c9a28eef31a2618f7185b80bd133ea4b2e3ed68b Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Thu, 18 Dec 2025 18:16:16 +0100 Subject: [PATCH 12/12] Add feedback message controls for Telegram bot Introduced configurable feedback message options for the Telegram bot in ApplicationProperties and settings.yml.template, allowing enabling/disabling of feedback messages for users and channels. Updated TelegramPipelineBot to respect the new feedback settings when sending messages. --- .../common/model/ApplicationProperties.java | 50 ++++++++++++++++--- .../service/telegram/TelegramPipelineBot.java | 26 ++++++---- .../src/main/resources/settings.yml.template | 11 ++++ 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 29e468020..f051bf622 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -336,8 +336,8 @@ public class ApplicationProperties { throw new UnsupportedProviderException( "Logout from the provider " + registrationId - + " is not supported. " - + "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues"); + + " is not supported. Report it at" + + " https://github.com/Stirling-Tools/Stirling-PDF/issues"); }; } } @@ -537,10 +537,10 @@ public class ApplicationProperties { @Override public String toString() { return """ - Driver { - driverName='%s' - } - """ + Driver { + driverName='%s' + } + """ .formatted(driverName); } } @@ -646,6 +646,44 @@ public class ApplicationProperties { private List allowChannelIDs = new ArrayList<>(); private long processingTimeoutSeconds = 180; private long pollingIntervalMillis = 2000; + private Feedback feedback = new Feedback(); + + @Data + public static class Feedback { + private General general = new General(); + private Channel channel = new Channel(); + private User user = new User(); + + @Data + public static class General { + private Boolean enabled = true; // set to 'true' to enable feedback messages + } + + @Data + public static class Channel { + private Boolean noValidDocument = + false; // set to 'true' to deny feedback messages in channels (to avoid + // spam) + private Boolean processingError = + false; // set to 'true' to deny feedback messages in channels (to avoid + // spam) + private Boolean errorMessage = + false; // set to 'true' to deny error feedback messages in channels (to + // avoid spam) + } + + @Data + public static class User { + private Boolean noValidDocument = + false; // set to 'true' to deny feedback messages to users (to avoid + // spam) + private Boolean processingError = + false; // set to 'true' to deny feedback messages to users (to avoid + // spam) + private Boolean errorMessage = + false; // set to 'true' to deny error feedback messages to users (to avoid + } + } } @Data diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index cb93226bf..f2ea1ea21 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -107,13 +107,17 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { if (update.hasMessage() && update.getMessage().hasText()) { String message_text = update.getMessage().getText(); long chat_id = update.getMessage().getChatId(); - if (message_text.equals("/start")) { + if ("/start".equals(message_text)) { sendMessage( chat_id, - "Welcome to the SPDF Telegram Bot!\n\n" - + "To get started, please send me a PDF document that you would like to process." - + " Make sure the document is in PDF format.\n\n" - + "Once I receive your document, I'll begin processing it through the pipeline."); + """ + Welcome to the SPDF Telegram Bot! + + To get started, please send me a PDF document that you would like to process. + Make sure the document is in PDF format. + + Once I receive your document, I'll begin processing it through the pipeline. + """); return; } } @@ -122,10 +126,11 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { handleIncomingFile(message); return; } - - sendMessage( - chat.getId(), - "No valid file found in the message. Please send a document to process."); + if (telegramProperties.getFeedback().getGeneral().getEnabled()) { + sendMessage( + chat.getId(), + "No valid file found in the message. Please send a document to process."); + } } // --------------------------- @@ -237,7 +242,8 @@ public class TelegramPipelineBot extends TelegramLongPollingBot { if (!hasJsonConfig(chatId)) { sendMessage( chatId, - "No JSON configuration file found in the pipeline inbox folder. Please contact the administrator."); + "No JSON configuration file found in the pipeline inbox folder. Please contact" + + " the administrator."); return; } diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index c7928c91a..f7c798241 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -124,6 +124,17 @@ telegram: allowChannelIDs: [] # List of allowed Telegram channel IDs (e.g. [-1001234567890, -1009876543210]). Leave empty to allow all channels. processingTimeoutSeconds: 180 # Maximum time in seconds to wait for processing a Telegram request pollingIntervalMillis: 2000 # Interval in milliseconds between polling for new messages + feedback: + general: + enabled: true # set to 'true' to enable feedback messages + channel: + noValidDocument: false # set to 'true' to deny feedback messages in channels (to avoid spam) + processingError: false # set to 'true' to deny feedback messages in channels (to avoid spam) + errorMessage: false # set to 'true' to deny error messages in channels (to avoid spam) + user: + noValidDocument: false # set to 'true' to deny feedback messages to users (to avoid spam) + processingError: false # set to 'true' to deny feedback messages to users (to avoid spam) + errorMessage: false # set to 'true' to deny error messages to users (to avoid spam) legal: termsAndConditions: https://www.stirling.com/legal/terms-of-service # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder