mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-01-14 20:11:17 +01:00
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.
This commit is contained in:
parent
7faf7e50fa
commit
a32316e517
@ -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;
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<Path> 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<Path> 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<Path> foundOutputs = new ArrayList<>();
|
||||
|
||||
while (Duration.between(start, Instant.now()).compareTo(timeout) <= 0) {
|
||||
try (Stream<Path> 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) {}
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user