Add Telegram bot integration for pipeline processing (#5185)

# Description of Changes

This pull request introduces Telegram bot integration to the
application, enabling users to send files via Telegram for processing
through the pipeline. The main changes add configuration options,
dependency management, and a new service for handling Telegram
interactions.

**Telegram bot integration:**

* Added a new `TelegramPipelineBot` service (`TelegramPipelineBot.java`)
that listens for incoming Telegram messages, downloads attached files or
photos, places them in a pipeline inbox folder, waits for processing
results, and sends the output files back to the user. The service
includes error handling and status messaging.
* Introduced a `TelegramBotConfig` configuration class to initialize and
register the Telegram bot only when enabled via application properties.
* Added a new `Telegram` configuration section to
`ApplicationProperties` and the `settings.yml.template`, supporting
options like enabling/disabling the bot, bot token/username, pipeline
folder, processing timeout, and polling interval.
[[1]](diffhunk://#diff-1c357db0a3e88cf5bedd4a5852415fadad83b8b3b9eb56e67059d8b9d8b10702R63)
[[2]](diffhunk://#diff-1c357db0a3e88cf5bedd4a5852415fadad83b8b3b9eb56e67059d8b9d8b10702R580-R589)
[[3]](diffhunk://#diff-12f23603ae35319a3ea08f91b6340d5d935216941fda2e69d2df1b6cd22a63f2R108-R115)

**Dependency management:**

* Added the `org.telegram:telegrambots` library to the project
dependencies to support Telegram bot functionality.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
Ludy
2026-01-13 22:14:59 +00:00
committed by GitHub
co-authored by Copilot Anthony Stirling
parent 83e96a9aa3
commit e7b030e6b5
13 changed files with 1313 additions and 84 deletions
@@ -61,6 +61,7 @@ public class ApplicationProperties {
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated(); private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
private Mail mail = new Mail(); private Mail mail = new Mail();
private Telegram telegram = new Telegram();
private Premium premium = new Premium(); private Premium premium = new Premium();
@@ -607,6 +608,7 @@ public class ApplicationProperties {
private boolean ssoAutoLogin; private boolean ssoAutoLogin;
private CustomMetadata customMetadata = new CustomMetadata(); private CustomMetadata customMetadata = new CustomMetadata();
@Deprecated
@Data @Data
public static class CustomMetadata { public static class CustomMetadata {
private boolean autoUpdateMetadata; private boolean autoUpdateMetadata;
@@ -614,16 +616,23 @@ public class ApplicationProperties {
private String creator; private String creator;
private String producer; private String producer;
@Deprecated
public String getCreator() { public String getCreator() {
return creator == null || creator.trim().isEmpty() ? "Stirling-PDF" : creator; return creator == null || creator.trim().isEmpty() ? "Stirling-PDF" : creator;
} }
@Deprecated
public String getProducer() { public String getProducer() {
return producer == null || producer.trim().isEmpty() ? "Stirling-PDF" : producer; return producer == null || producer.trim().isEmpty() ? "Stirling-PDF" : producer;
} }
} }
} }
/**
* Mail server configuration properties.
*
* @since 0.46.1
*/
@Data @Data
public static class Mail { public static class Mail {
private boolean enabled; private boolean enabled;
@@ -646,6 +655,102 @@ public class ApplicationProperties {
private Boolean sslCheckServerIdentity; private Boolean sslCheckServerIdentity;
} }
/**
* Telegram bot configuration properties.
*
* @since 2.2.x
*/
@Data
public static class Telegram {
private Boolean enabled = false;
@ToString.Exclude private String botToken;
private String botUsername;
private String pipelineInboxFolder = "telegram";
private Boolean customFolderSuffix = false;
private Boolean enableAllowUserIDs = false;
private List<Long> allowUserIDs = new ArrayList<>();
private Boolean enableAllowChannelIDs = false;
private List<Long> allowChannelIDs = new ArrayList<>();
private long processingTimeoutSeconds = 180;
private long pollingIntervalMillis = 2000;
private Feedback feedback = new Feedback();
/**
* Configuration for feedback messages sent by the Telegram bot.
*
* @since 2.2.x
*/
@Data
public static class Feedback {
private Channel channel = new Channel();
private User user = new User();
/**
* Channel-specific feedback settings.
*
* @since 2.2.x
*/
@Data
public static class Channel {
/**
* Set to {@code false} to hide/suppress "no valid document" feedback messages to
* the channel (to avoid spam).
*/
private Boolean noValidDocument = true;
/**
* Set to {@code false} to hide/suppress generic error feedback messages to the
* channel (to avoid spam).
*/
private Boolean errorMessage = true;
/**
* Set to {@code false} to hide/suppress processing error feedback messages to the
* channel (to avoid spam).
*/
private Boolean errorProcessing = true;
/**
* Set to {@code false} to hide/suppress "processing" feedback messages to the
* channel (to avoid spam).
*/
private Boolean processing = true;
}
/**
* User-specific feedback settings.
*
* @since 2.2.x
*/
@Data
public static class User {
/**
* Set to {@code false} to hide/suppress "no valid document" feedback messages to
* users (to avoid spam).
*/
private Boolean noValidDocument = true;
/**
* Set to {@code false} to hide/suppress generic error feedback messages to users
* (to avoid spam).
*/
private Boolean errorMessage = true;
/**
* Set to {@code false} to hide/suppress processing error feedback messages to users
* (to avoid spam).
*/
private Boolean errorProcessing = true;
/**
* Set to {@code false} to hide/suppress "processing" feedback messages to users (to
* avoid spam).
*/
private Boolean processing = true;
}
}
}
@Data @Data
public static class Premium { public static class Premium {
private boolean enabled; private boolean enabled;
+1
View File
@@ -59,6 +59,7 @@ dependencies {
implementation project(':common') implementation project(':common')
implementation 'org.springframework.boot:spring-boot-starter-jetty' implementation 'org.springframework.boot:spring-boot-starter-jetty'
implementation 'com.posthog.java:posthog:1.2.0' implementation 'com.posthog.java:posthog:1.2.0'
implementation 'org.telegram:telegrambots:6.9.7.1'
implementation 'commons-io:commons-io:2.21.0' implementation 'commons-io:commons-io:2.21.0'
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
@@ -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,20 @@
package stirling.software.SPDF.service.telegram;
/**
* Enumeration representing different feedback types for Telegram service.
*
* @since 2.2.x
*/
public enum FeedbackEnum {
/** Indicates that the provided document is not valid. */
NO_VALID_DOCUMENT,
/** Represents a generic error message. */
ERROR_MESSAGE,
/** Indicates that an error occurred during processing. */
ERROR_PROCESSING,
/** Indicates that processing is ongoing. */
PROCESSING
}
@@ -0,0 +1,524 @@
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.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
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.Update;
import org.telegram.telegrambots.meta.api.objects.User;
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;
/**
* Telegram bot that processes incoming files through a defined pipeline.
*
* @since 2.2.x
*/
@Slf4j
@Component
@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 Set<String> SUPPORTED_CHAT_TYPES =
Set.of(CHAT_PRIVATE, CHAT_GROUP, CHAT_SUPERGROUP, CHAT_CHANNEL);
private static final Set<String> ALLOWED_MIME_TYPES = Set.of("application/pdf");
private final Object pipelinePollMonitor = new Object();
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 = extractMessage(update);
if (message == null) {
return;
}
Chat chat = message.getChat();
if (chat == null || !isSupportedChatType(chat.getType())) {
log.info(
"Ignoring message {}, unsupported chat type {}",
message.getMessageId(),
chat != null ? chat.getType() : "null");
return;
}
if (!isAuthorized(message, chat)) {
return;
}
if (update.hasMessage() && update.getMessage().hasText()) {
String messageText = update.getMessage().getText();
long chatId = update.getMessage().getChatId();
if ("/start".equals(messageText)) {
sendMessage(
chatId,
"""
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;
}
}
if (message.hasDocument()) {
handleIncomingFile(message);
return;
}
if (feedback(FeedbackEnum.NO_VALID_DOCUMENT, chat.getType())) {
sendMessage(
chat.getId(),
"No valid file found in the message. Please send a document to process.");
}
}
private boolean feedback(FeedbackEnum feedbackEnum, String chatType) {
return switch (feedbackEnum) {
case NO_VALID_DOCUMENT ->
switch (chatType) {
case CHAT_CHANNEL ->
telegramProperties.getFeedback().getChannel().getNoValidDocument();
case CHAT_PRIVATE ->
telegramProperties.getFeedback().getUser().getNoValidDocument();
default -> true;
};
case ERROR_MESSAGE ->
switch (chatType) {
case CHAT_CHANNEL ->
telegramProperties.getFeedback().getChannel().getErrorMessage();
case CHAT_PRIVATE ->
telegramProperties.getFeedback().getUser().getErrorMessage();
default -> true;
};
case ERROR_PROCESSING ->
switch (chatType) {
case CHAT_CHANNEL ->
telegramProperties.getFeedback().getChannel().getErrorProcessing();
case CHAT_PRIVATE ->
telegramProperties.getFeedback().getUser().getErrorProcessing();
default -> true;
};
case PROCESSING ->
switch (chatType) {
case CHAT_CHANNEL ->
telegramProperties.getFeedback().getChannel().getProcessing();
case CHAT_PRIVATE ->
telegramProperties.getFeedback().getUser().getProcessing();
default -> true;
};
default -> true;
};
}
// ---------------------------
// 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 type != null && SUPPORTED_CHAT_TYPES.contains(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<Long> 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());
if (feedback(FeedbackEnum.ERROR_MESSAGE, chat.getType())) {
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<Long> 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());
if (feedback(FeedbackEnum.ERROR_MESSAGE, chat.getType())) {
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();
String chatType = message.getChat().getType();
if (doc == null) {
if (feedback(FeedbackEnum.NO_VALID_DOCUMENT, chatType)) {
sendMessage(chatId, "No document found.");
}
return;
}
if (doc.getMimeType() != null
&& !ALLOWED_MIME_TYPES.contains(doc.getMimeType().toLowerCase())) {
if (feedback(FeedbackEnum.NO_VALID_DOCUMENT, chatType)) {
sendMessage(
chatId,
"Unsupported MIME type: "
+ doc.getMimeType()
+ "\nAllowed: "
+ String.join(", ", ALLOWED_MIME_TYPES));
}
return;
}
if (!hasJsonConfig(chatId)) {
if (feedback(FeedbackEnum.ERROR_PROCESSING, chatType)) {
sendMessage(
chatId,
"No JSON configuration file found in the pipeline inbox folder. Please"
+ " contact the administrator.");
}
return;
}
try {
if (!CHAT_CHANNEL.equalsIgnoreCase(chatType)
&& feedback(FeedbackEnum.PROCESSING, chatType)) {
sendMessage(chatId, "File received. Starting processing...");
}
PipelineFileInfo info = downloadMessageFile(message);
List<Path> outputs = waitForPipelineOutputs(info);
if (outputs.isEmpty()) {
if (feedback(FeedbackEnum.ERROR_PROCESSING, chatType)) {
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);
if (feedback(FeedbackEnum.ERROR_MESSAGE, chatType)) {
sendMessage(chatId, "Telegram API error occurred.");
}
} catch (IOException e) {
log.error("IO error", e);
if (feedback(FeedbackEnum.ERROR_MESSAGE, chatType)) {
sendMessage(chatId, "An IO error occurred.");
}
} catch (Exception e) {
log.error("Unexpected error", e);
if (feedback(FeedbackEnum.ERROR_MESSAGE, chatType)) {
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 {
try {
URI uri =
new URI(
"https",
"api.telegram.org",
"/file/bot" + getBotToken() + "/" + filePath,
null);
return uri.toURL();
} catch (URISyntaxException e) {
throw new MalformedURLException("Failed to build Telegram download URL");
} catch (MalformedURLException e) {
MalformedURLException sanitized =
new MalformedURLException("Failed to build Telegram download URL");
sanitized.initCause(e);
throw sanitized;
}
}
// ---------------------------
// 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<Path> 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<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 poll = Duration.ofMillis(telegramProperties.getPollingIntervalMillis());
List<Path> results = new ArrayList<>();
while (Duration.between(start, Instant.now()).compareTo(timeout) <= 0) {
try (Stream<Path> s = Files.list(finishedDir)) {
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.info("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();
}
@Override
public String getBotToken() {
return telegramProperties.getBotToken();
}
}
@@ -16,30 +16,30 @@ security:
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2) loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
initialLogin: initialLogin:
username: '' # initial username for the first login username: "" # initial username for the first login
password: '' # initial password for the first login password: "" # initial password for the first login
oauth2: oauth2:
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work) enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
client: client:
keycloak: keycloak:
issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint issuer: "" # URL of the Keycloak realm's OpenID Connect Discovery endpoint
clientId: '' # client ID for Keycloak OAuth2 clientId: "" # client ID for Keycloak OAuth2
clientSecret: '' # client secret for Keycloak OAuth2 clientSecret: "" # client secret for Keycloak OAuth2
scopes: openid, profile, email # scopes for Keycloak OAuth2 scopes: openid, profile, email # scopes for Keycloak OAuth2
useAsUsername: preferred_username # field to use as the username for Keycloak OAuth2. Available options are: [email | name | given_name | family_name | preferred_name] useAsUsername: preferred_username # field to use as the username for Keycloak OAuth2. Available options are: [email | name | given_name | family_name | preferred_name]
google: google:
clientId: '' # client ID for Google OAuth2 clientId: "" # client ID for Google OAuth2
clientSecret: '' # client secret for Google OAuth2 clientSecret: "" # client secret for Google OAuth2
scopes: email, profile # scopes for Google OAuth2 scopes: email, profile # scopes for Google OAuth2
useAsUsername: email # field to use as the username for Google OAuth2. Available options are: [email | name | given_name | family_name] useAsUsername: email # field to use as the username for Google OAuth2. Available options are: [email | name | given_name | family_name]
github: github:
clientId: '' # client ID for GitHub OAuth2 clientId: "" # client ID for GitHub OAuth2
clientSecret: '' # client secret for GitHub OAuth2 clientSecret: "" # client secret for GitHub OAuth2
scopes: read:user # scope for GitHub OAuth2 scopes: read:user # scope for GitHub OAuth2
useAsUsername: login # field to use as the username for GitHub OAuth2. Available options are: [email | login | name] useAsUsername: login # field to use as the username for GitHub OAuth2. Available options are: [email | login | name]
issuer: '' # set to any Provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint issuer: "" # set to any Provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
clientId: '' # client ID from your Provider clientId: "" # client ID from your Provider
clientSecret: '' # client secret from your Provider clientSecret: "" # client secret from your Provider
autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
useAsUsername: email # default is 'email'; custom fields can be used as the username useAsUsername: email # default is 'email'; custom fields can be used as the username
@@ -47,14 +47,14 @@ security:
provider: google # set this to your OAuth Provider's name, e.g., 'google' or 'keycloak' provider: google # set this to your OAuth Provider's name, e.g., 'google' or 'keycloak'
saml2: saml2:
enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true) enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true)
provider: '' # The name of your Provider provider: "" # The name of your Provider
autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
registrationId: stirling # The name of your Service Provider (SP) app name. Should match the name in the path for your SSO & SLO URLs registrationId: stirling # The name of your Service Provider (SP) app name. Should match the name in the path for your SSO & SLO URLs
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # The uri for your Provider's metadata idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # The uri for your Provider's metadata
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml # The URL for initiating SSO. Provided by your Provider idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml # The URL for initiating SSO. Provided by your Provider
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml # The URL for initiating SLO. Provided by your Provider idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml # The URL for initiating SLO. Provided by your Provider
idpIssuer: '' # The ID of your Provider idpIssuer: "" # The ID of your Provider
idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
@@ -110,21 +110,45 @@ mail:
enableInvites: false # set to 'true' to enable email invites for user management (requires mail.enabled and security.enableLogin) enableInvites: false # set to 'true' to enable email invites for user management (requires mail.enabled and security.enableLogin)
host: smtp.example.com # SMTP server hostname host: smtp.example.com # SMTP server hostname
port: 587 # SMTP server port port: 587 # SMTP server port
username: '' # SMTP server username username: "" # SMTP server username
password: '' # SMTP server password password: "" # SMTP server password
from: '' # sender email address from: "" # sender email address
startTlsEnable: true # enable STARTTLS (explicit TLS upgrade after connecting) when supported by the SMTP server startTlsEnable: true # enable STARTTLS (explicit TLS upgrade after connecting) when supported by the SMTP server
startTlsRequired: false # require STARTTLS; connection fails if the upgrade command is not supported startTlsRequired: false # require STARTTLS; connection fails if the upgrade command is not supported
sslEnable: false # enable SSL/TLS wrapper for implicit TLS (typically used with port 465) sslEnable: false # enable SSL/TLS wrapper for implicit TLS (typically used with port 465)
sslTrust: '' # optional trusted host override, e.g. "smtp.example.com" or "*"; defaults to "*" (trust all) when empty sslTrust: "" # optional trusted host override, e.g. "smtp.example.com" or "*"; defaults to "*" (trust all) when empty
sslCheckServerIdentity: false # enable hostname verification when using SSL/TLS sslCheckServerIdentity: false # enable hostname verification when using SSL/TLS
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
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
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:
channel:
noValidDocument: true # set to 'false' to hide/suppress feedback messages in channels (to avoid spam)
errorProcessing: true # set to 'false' to hide/suppress feedback messages in channels (to avoid spam)
errorMessage: true # set to 'false' to hide/suppress error messages in channels (to avoid spam)
processing: true # set to 'false' to hide/suppress processing messages in channels (to avoid spam)
user:
noValidDocument: true # set to 'false' to hide/suppress feedback messages to users (to avoid spam)
errorProcessing: true # set to 'false' to hide/suppress feedback messages to users (to avoid spam)
errorMessage: true # set to 'false' to hide/suppress error messages to users (to avoid spam)
processing: true # set to 'false' to hide/suppress processing messages to users (to avoid spam)
legal: 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 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 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
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder accessibilityStatement: "" # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder cookiePolicy: "" # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder impressum: "" # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
system: system:
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc) defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
@@ -143,8 +167,8 @@ system:
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML) disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
maxDPI: 500 # Maximum allowed DPI for PDF to image conversion maxDPI: 500 # Maximum allowed DPI for PDF to image conversion
corsAllowedOrigins: [] # List of allowed origins for CORS (e.g. ['http://localhost:5173', 'https://app.example.com']). Leave empty to disable CORS. For local development with frontend on port 5173, add 'http://localhost:5173' corsAllowedOrigins: [] # List of allowed origins for CORS (e.g. ['http://localhost:5173', 'https://app.example.com']). Leave empty to disable CORS. For local development with frontend on port 5173, add 'http://localhost:5173'
backendUrl: '' # Backend base URL for SAML/OAuth/API callbacks (e.g. 'http://localhost:8080' for dev, 'https://api.example.com' for production). REQUIRED for SSO authentication to work correctly. This is where your IdP will send SAML responses and OAuth callbacks. Leave empty to default to 'http://localhost:8080' in development. backendUrl: "" # Backend base URL for SAML/OAuth/API callbacks (e.g. 'http://localhost:8080' for dev, 'https://api.example.com' for production). REQUIRED for SSO authentication to work correctly. This is where your IdP will send SAML responses and OAuth callbacks. Leave empty to default to 'http://localhost:8080' in development.
frontendUrl: '' # Frontend URL for invite email links (e.g. 'https://app.example.com'). Optional - if not set, will use backendUrl. This is the URL users click in invite emails. frontendUrl: "" # Frontend URL for invite email links (e.g. 'https://app.example.com'). Optional - if not set, will use backendUrl. This is the URL users click in invite emails.
enableMobileScanner: false # Enable mobile phone QR code upload feature. Requires frontendUrl to be configured. enableMobileScanner: false # Enable mobile phone QR code upload feature. Requires frontendUrl to be configured.
mobileScannerSettings: mobileScannerSettings:
convertToPdf: true # Automatically convert uploaded images to PDF format. If false, images are kept as-is. convertToPdf: true # Automatically convert uploaded images to PDF format. If false, images are kept as-is.
@@ -162,14 +186,14 @@ system:
level: MEDIUM # Security level: MAX (whitelist only), MEDIUM (block internal networks), OFF (no restrictions) level: MEDIUM # Security level: MAX (whitelist only), MEDIUM (block internal networks), OFF (no restrictions)
allowedDomains: [] # Whitelist of allowed domains (e.g. ['cdn.example.com', 'images.google.com']) allowedDomains: [] # Whitelist of allowed domains (e.g. ['cdn.example.com', 'images.google.com'])
blockedDomains: [] # Additional domains to block (e.g. ['evil.com', 'malicious.org']) blockedDomains: [] # Additional domains to block (e.g. ['evil.com', 'malicious.org'])
internalTlds: ['.local', '.internal', '.corp', '.home'] # Block domains with these TLD patterns internalTlds: [".local", ".internal", ".corp", ".home"] # Block domains with these TLD patterns
blockPrivateNetworks: true # Block RFC 1918 private networks (10.x.x.x, 192.168.x.x, 172.16-31.x.x) blockPrivateNetworks: true # Block RFC 1918 private networks (10.x.x.x, 192.168.x.x, 172.16-31.x.x)
blockLocalhost: true # Block localhost and loopback addresses (127.x.x.x, ::1) blockLocalhost: true # Block localhost and loopback addresses (127.x.x.x, ::1)
blockLinkLocal: true # Block link-local addresses (169.254.x.x, fe80::/10) blockLinkLocal: true # Block link-local addresses (169.254.x.x, fe80::/10)
blockCloudMetadata: true # Block cloud provider metadata endpoints (169.254.169.254) blockCloudMetadata: true # Block cloud provider metadata endpoints (169.254.169.254)
datasource: datasource:
enableCustomDatabase: false # Enterprise users ONLY, set this property to 'true' if you would like to use your own custom database configuration enableCustomDatabase: false # Enterprise users ONLY, set this property to 'true' if you would like to use your own custom database configuration
customDatabaseUrl: '' # eg jdbc:postgresql://localhost:5432/postgres, set the url for your own custom database connection. If provided, the type, hostName, port and name are not necessary and will not be used customDatabaseUrl: "" # eg jdbc:postgresql://localhost:5432/postgres, set the url for your own custom database connection. If provided, the type, hostName, port and name are not necessary and will not be used
username: postgres # set the database username username: postgres # set the database username
password: postgres # set the database password password: postgres # set the database password
type: postgresql # the type of the database to set (e.g. 'h2', 'postgresql') type: postgresql # the type of the database to set (e.g. 'h2', 'postgresql')
@@ -178,29 +202,29 @@ system:
name: postgres # set the name of your database. Should match the name of the database you create name: postgres # set the name of your database. Should match the name of the database you create
customPaths: customPaths:
pipeline: pipeline:
watchedFoldersDir: '' # Defaults to /pipeline/watchedFolders watchedFoldersDir: "" # Defaults to /pipeline/watchedFolders
finishedFoldersDir: '' # Defaults to /pipeline/finishedFolders finishedFoldersDir: "" # Defaults to /pipeline/finishedFolders
operations: operations:
weasyprint: '' # Defaults to /opt/venv/bin/weasyprint weasyprint: "" # Defaults to /opt/venv/bin/weasyprint
unoconvert: '' # Defaults to /opt/venv/bin/unoconvert unoconvert: "" # Defaults to /opt/venv/bin/unoconvert
calibre: '' # Defaults to /usr/bin/ebook-convert calibre: "" # Defaults to /usr/bin/ebook-convert
ocrmypdf: '' # Defaults to /usr/bin/ocrmypdf ocrmypdf: "" # Defaults to /usr/bin/ocrmypdf
soffice: '' # Defaults to /usr/bin/soffice soffice: "" # Defaults to /usr/bin/soffice
fileUploadLimit: '' # Defaults to "". No limit when string is empty. Set a number, between 0 and 999, followed by one of the following strings to set a limit. "KB", "MB", "GB". fileUploadLimit: "" # Defaults to "". No limit when string is empty. Set a number, between 0 and 999, followed by one of the following strings to set a limit. "KB", "MB", "GB".
tempFileManagement: tempFileManagement:
baseTmpDir: '' # Defaults to java.io.tmpdir/stirling-pdf baseTmpDir: "" # Defaults to java.io.tmpdir/stirling-pdf
libreofficeDir: '' # Defaults to tempFileManagement.baseTmpDir/libreoffice libreofficeDir: "" # Defaults to tempFileManagement.baseTmpDir/libreoffice
systemTempDir: '' # Only used if cleanupSystemTemp is true systemTempDir: "" # Only used if cleanupSystemTemp is true
prefix: stirling-pdf- # Prefix for temp file names prefix: stirling-pdf- # Prefix for temp file names
maxAgeHours: 24 # Maximum age in hours before temp files are cleaned up maxAgeHours: 24 # Maximum age in hours before temp files are cleaned up
cleanupIntervalMinutes: 30 # How often to run cleanup (in minutes) cleanupIntervalMinutes: 30 # How often to run cleanup (in minutes)
startupCleanup: true # Clean up old temp files on startup startupCleanup: true # Clean up old temp files on startup
cleanupSystemTemp: false # Whether to clean broader system temp directory cleanupSystemTemp: false # Whether to clean broader system temp directory
databaseBackup: databaseBackup:
cron: '0 0 0 * * ?' # Cron expression for automatic database backups "0 0 0 * * ?" daily at midnight cron: "0 0 0 * * ?" # Cron expression for automatic database backups "0 0 0 * * ?" daily at midnight
ui: ui:
appNameNavbar: '' # name displayed on the navigation bar appNameNavbar: "" # name displayed on the navigation bar
logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo) logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo)
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled. languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
@@ -581,6 +581,7 @@ public class AdminSettingsController {
case "processexecutor", "processExecutor" -> applicationProperties.getProcessExecutor(); case "processexecutor", "processExecutor" -> applicationProperties.getProcessExecutor();
case "autopipeline", "autoPipeline" -> applicationProperties.getAutoPipeline(); case "autopipeline", "autoPipeline" -> applicationProperties.getAutoPipeline();
case "legal" -> applicationProperties.getLegal(); case "legal" -> applicationProperties.getLegal();
case "telegram" -> applicationProperties.getTelegram();
default -> null; default -> null;
}; };
} }
@@ -602,7 +603,8 @@ public class AdminSettingsController {
"processexecutor", "processexecutor",
"autoPipeline", "autoPipeline",
"autopipeline", "autopipeline",
"legal"); "legal",
"telegram");
// Pattern to validate safe property paths - only alphanumeric, dots, and underscores // Pattern to validate safe property paths - only alphanumeric, dots, and underscores
private static final Pattern SAFE_KEY_PATTERN = private static final Pattern SAFE_KEY_PATTERN =
+125 -28
View File
@@ -82,7 +82,7 @@ incorrectPasswordMessage = "Das Passwort ist falsch."
usernameExistsMessage = "Neuer Benutzername existiert bereits." usernameExistsMessage = "Neuer Benutzername existiert bereits."
invalidUsernameMessage = "Ungültiger Benutzername. Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein." invalidUsernameMessage = "Ungültiger Benutzername. Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein."
invalidPasswordMessage = "Das Passwort darf nicht leer sein und kein Leerzeichen am Anfang und Ende haben." invalidPasswordMessage = "Das Passwort darf nicht leer sein und kein Leerzeichen am Anfang und Ende haben."
confirmPasswordErrorMessage = "Neues Passwort und Neues Passwort bestätigen müssen übereinstimmen." confirmPasswordErrorMessage = "\"Neues Passwort\" und \"Neues Passwort bestätigen\" müssen übereinstimmen."
deleteCurrentUserMessage = "Der aktuell angemeldete Benutzer kann nicht gelöscht werden." deleteCurrentUserMessage = "Der aktuell angemeldete Benutzer kann nicht gelöscht werden."
deleteUsernameExistsMessage = "Der Benutzername existiert nicht und kann nicht gelöscht werden." deleteUsernameExistsMessage = "Der Benutzername existiert nicht und kann nicht gelöscht werden."
downgradeCurrentUserMessage = "Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden" downgradeCurrentUserMessage = "Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden"
@@ -363,6 +363,7 @@ advanced = "Erweitert"
title = "Sicherheit & Authentifizierung" title = "Sicherheit & Authentifizierung"
security = "Sicherheit" security = "Sicherheit"
connections = "Verbindungen" connections = "Verbindungen"
telegram = "Telegram"
[settings.licensingAnalytics] [settings.licensingAnalytics]
title = "Lizenzierung & Analytics" title = "Lizenzierung & Analytics"
@@ -2316,7 +2317,7 @@ colorLabel = "Textfarbe"
heading = "Gespeicherte Unterschriften" heading = "Gespeicherte Unterschriften"
description = "Gespeicherte Unterschriften jederzeit wiederverwenden." description = "Gespeicherte Unterschriften jederzeit wiederverwenden."
emptyTitle = "Noch keine gespeicherten Unterschriften" emptyTitle = "Noch keine gespeicherten Unterschriften"
emptyDescription = "Zeichnen, laden oder tippen Sie oben eine Unterschrift und wählen Sie dann In Bibliothek speichern, um bis zu {{max}} Favoriten bereitzuhalten." emptyDescription = "Zeichnen, laden oder tippen Sie oben eine Unterschrift und wählen Sie dann \"In Bibliothek speichern\", um bis zu {{max}} Favoriten bereitzuhalten."
limitTitle = "Limit erreicht" limitTitle = "Limit erreicht"
limitDescription = "Entfernen Sie eine gespeicherte Unterschrift, bevor Sie neue hinzufügen (max. {{max}})." limitDescription = "Entfernen Sie eine gespeicherte Unterschrift, bevor Sie neue hinzufügen (max. {{max}})."
carouselPosition = "{{current}} von {{total}}" carouselPosition = "{{current}} von {{total}}"
@@ -2526,7 +2527,7 @@ title = "Über Anmerkungen entfernen"
description = "Dieses Werkzeug entfernt alle Anmerkungen (Kommentare, Hervorhebungen, Notizen usw.) aus Ihren PDF-Dokumenten." description = "Dieses Werkzeug entfernt alle Anmerkungen (Kommentare, Hervorhebungen, Notizen usw.) aus Ihren PDF-Dokumenten."
[removeAnnotations.tooltip.header] [removeAnnotations.tooltip.header]
title = "Über Anmerkungen entfernen" title = "Über \"Anmerkungen entfernen\""
[removeAnnotations.tooltip.description] [removeAnnotations.tooltip.description]
title = "Funktion" title = "Funktion"
@@ -3849,11 +3850,6 @@ failed = "Ein Fehler ist beim Komprimieren der PDF aufgetreten."
_value = "Kompressionseinstellungen" _value = "Kompressionseinstellungen"
1 = "1-3 PDF-Komprimierung, </br> 4-6 Leichte Bildkomprimierung, </br> 7-9 Intensive Bildkomprimierung verringert die Bildqualität dramatisch" 1 = "1-3 PDF-Komprimierung, </br> 4-6 Leichte Bildkomprimierung, </br> 7-9 Intensive Bildkomprimierung verringert die Bildqualität dramatisch"
[compress.compressionLevel]
range1to3 = "Lower values preserve quality but result in larger files"
range4to6 = "Medium compression with moderate quality reduction"
range7to9 = "Higher values reduce file size significantly but may reduce image clarity"
[compress.lineArt] [compress.lineArt]
description = "Uses ImageMagick to reduce pages to high-contrast black and white for maximum size reduction." description = "Uses ImageMagick to reduce pages to high-contrast black and white for maximum size reduction."
detailLevel = "Detail level" detailLevel = "Detail level"
@@ -3867,6 +3863,11 @@ unavailable = "ImageMagick is not installed or enabled on this server"
[compress.linearize] [compress.linearize]
label = "Linearize PDF for fast web viewing" label = "Linearize PDF for fast web viewing"
[compress.compressionLevel]
range1to3 = "Lower values preserve quality but result in larger files"
range4to6 = "Medium compression with moderate quality reduction"
range7to9 = "Higher values reduce file size significantly but may reduce image clarity"
[decrypt] [decrypt]
passwordPrompt = "Diese Datei ist passwortgeschützt. Bitte geben Sie das Passwort ein:" passwordPrompt = "Diese Datei ist passwortgeschützt. Bitte geben Sie das Passwort ein:"
cancelled = "Vorgang für PDF abgebrochen: {0}" cancelled = "Vorgang für PDF abgebrochen: {0}"
@@ -4020,7 +4021,7 @@ showPreferencesBtn = "Einstellungen verwalten"
[cookieBanner.popUp.description] [cookieBanner.popUp.description]
1 = "Wir verwenden Cookies und andere Technologien, damit Stirling PDF für Sie besser funktioniert. Dies hilft uns dabei, unsere Tools zu verbessern und weiterhin Funktionen zu entwickeln, die Ihnen gefallen werden." 1 = "Wir verwenden Cookies und andere Technologien, damit Stirling PDF für Sie besser funktioniert. Dies hilft uns dabei, unsere Tools zu verbessern und weiterhin Funktionen zu entwickeln, die Ihnen gefallen werden."
2 = "Wenn Sie dies nicht möchten, klicken Sie auf Nein, Danke. Dadurch werden nur die unbedingt erforderlichen Cookies aktiviert, die für einen reibungslosen Ablauf erforderlich sind." 2 = "Wenn Sie dies nicht möchten, klicken Sie auf \"Nein, Danke\". Dadurch werden nur die unbedingt erforderlichen Cookies aktiviert, die für einen reibungslosen Ablauf erforderlich sind."
[cookieBanner.preferencesModal] [cookieBanner.preferencesModal]
title = "Einwilligungszentrum" title = "Einwilligungszentrum"
@@ -4119,13 +4120,13 @@ toggleSidebar = "Seitenleiste umschalten"
exportSelected = "Ausgewählte Seiten exportieren" exportSelected = "Ausgewählte Seiten exportieren"
toggleAnnotations = "Anmerkungen ein-/ausblenden" toggleAnnotations = "Anmerkungen ein-/ausblenden"
print = "PDF drucken" print = "PDF drucken"
draw = "Zeichnen"
save = "Speichern"
saveChanges = "Änderungen speichern" saveChanges = "Änderungen speichern"
annotations = "Annotations" annotations = "Annotations"
applyRedactionsFirst = "Apply redactions first" applyRedactionsFirst = "Apply redactions first"
draw = "Draw"
exitRedaction = "Exit Redaction Mode" exitRedaction = "Exit Redaction Mode"
redact = "Redact" redact = "Redact"
save = "Save"
[search] [search]
title = "PDF durchsuchen" title = "PDF durchsuchen"
@@ -4574,6 +4575,102 @@ pageFormatA4 = "A4 (210×297mm)"
pageFormatKeep = "Keep (Original Dimensions)" pageFormatKeep = "Keep (Original Dimensions)"
pageFormatLetter = "Letter (8.5×11in)" pageFormatLetter = "Letter (8.5×11in)"
[admin.settings.telegram]
title = "Telegram-Bot"
description = "Telegram-Bot-Anbindung, Zugriffskontrollen und Feedback-Verhalten konfigurieren."
[admin.settings.telegram.enabled]
label = "Telegram-Bot aktivieren"
description = "Erlaubt Benutzern die Interaktion mit Stirling PDF über den konfigurierten Telegram-Bot."
[admin.settings.telegram.botUsername]
label = "Bot-Benutzername"
description = "Der öffentliche Benutzername Ihres Telegram-Bots."
[admin.settings.telegram.botToken]
label = "Bot-Token"
description = "API-Token, das von BotFather für Ihren Telegram-Bot bereitgestellt wird."
[admin.settings.telegram.pipelineInboxFolder]
label = "Posteingangsordner"
description = "Ordner innerhalb des Pipeline-Verzeichnisses, in dem eingehende Telegram-Dateien gespeichert werden."
[admin.settings.telegram.customFolderSuffix]
label = "Benutzerdefinierten Ordner-Suffix verwenden"
description = "Hängt die Chat-ID an eingehende Dateiordner an, um Uploads pro Chat zu isolieren."
[admin.settings.telegram.accessControl]
title = "Zugriffskontrolle"
description = "Einschränken, welche Benutzer oder Kanäle mit dem Bot interagieren dürfen."
[admin.settings.telegram.enableAllowUserIDs]
label = "Bestimmte Benutzer-IDs erlauben"
description = "Wenn aktiviert, dürfen nur aufgeführte Benutzer-IDs den Bot verwenden."
[admin.settings.telegram.allowUserIDs]
label = "Erlaubte Benutzer-IDs"
description = "Telegram-Benutzer-IDs, die mit dem Bot interagieren dürfen."
placeholder = "Benutzer-ID hinzufügen und Enter drücken"
[admin.settings.telegram.enableAllowChannelIDs]
label = "Bestimmte Kanal-IDs erlauben"
description = "Wenn aktiviert, dürfen nur aufgeführte Kanal-IDs den Bot verwenden."
[admin.settings.telegram.allowChannelIDs]
label = "Erlaubte Kanal-IDs"
description = "Telegram-Kanal-IDs, die mit dem Bot interagieren dürfen."
placeholder = "Kanal-ID hinzufügen und Enter drücken"
[admin.settings.telegram.processing]
title = "Verarbeitung"
description = "Polling-Intervalle und Verarbeitungs-Timeouts für Telegram-Uploads steuern."
[admin.settings.telegram.processingTimeoutSeconds]
label = "Verarbeitungs-Timeout (Sekunden)"
description = "Maximale Wartezeit für einen Verarbeitungsjob, bevor ein Fehler gemeldet wird."
[admin.settings.telegram.pollingIntervalMillis]
label = "Polling-Intervall (ms)"
description = "Intervall zwischen Prüfungen auf neue Telegram-Updates."
[admin.settings.telegram.feedback]
title = "Feedback-Nachrichten"
description = "Festlegen, wann der Bot Feedback an Benutzer und Kanäle senden soll."
[admin.settings.telegram.feedback.general.enabled]
label = "Feedback aktivieren"
description = "Legt fest, ob der Bot überhaupt Feedback-Nachrichten sendet."
[admin.settings.telegram.feedback.channel]
title = "Kanal-Feedback-Regeln"
[admin.settings.telegram.feedback.channel.noValidDocument]
label = "\"Kein gültiges Dokument\" einblenden (Kanal)"
description = "Sendet die Meldung \"Kein gültiges Dokument\" bei Kanal-Uploads."
[admin.settings.telegram.feedback.channel.errorProcessing]
label = "Verarbeitungsfehler einblenden (Kanal)"
description = "Sendet Verarbeitungsfehlermeldungen an Kanäle senden."
[admin.settings.telegram.feedback.channel.errorMessage]
label = "Fehlermeldungen einblenden (Kanal)"
description = "Detaillierte Fehlermeldungen für Kanäle einblenden."
[admin.settings.telegram.feedback.user]
title = "Benutzer-Feedback-Regeln"
[admin.settings.telegram.feedback.user.noValidDocument]
label = "\"Kein gültiges Dokument\" einblenden (Benutzer)"
description = "Sendet die Meldung \"Kein gültiges Dokument\" bei Benutzer-Uploads."
[admin.settings.telegram.feedback.user.errorProcessing]
label = "Verarbeitungsfehler einblenden (Benutzer)"
description = "Sendet Verarbeitungsfehlermeldungen an Benutzer."
[admin.settings.telegram.feedback.user.errorMessage]
label = "Fehlermeldungen einblenden (Benutzer)"
description = "Detaillierte Fehlermeldungen für Benutzer einblenden."
[admin.settings.database] [admin.settings.database]
title = "Datenbank" title = "Datenbank"
description = "Benutzerdefinierte Datenbankverbindungseinstellungen für Enterprise-Bereitstellungen konfigurieren." description = "Benutzerdefinierte Datenbankverbindungseinstellungen für Enterprise-Bereitstellungen konfigurieren."
@@ -4842,11 +4939,11 @@ description = "Optionale Funktionen und Features konfigurieren."
[admin.settings.features.serverCertificate] [admin.settings.features.serverCertificate]
label = "Serverzertifikat" label = "Serverzertifikat"
description = "Serverseitige Zertifikatserstellung für die Funktion Mit Stirling-PDF signieren konfigurieren" description = "Serverseitige Zertifikatserstellung für die Funktion \"Mit Stirling-PDF signieren\" konfigurieren"
[admin.settings.features.serverCertificate.enabled] [admin.settings.features.serverCertificate.enabled]
label = "Serverzertifikat aktivieren" label = "Serverzertifikat aktivieren"
description = "Serverseitiges Zertifikat für die Option Mit Stirling-PDF signieren aktivieren" description = "Serverseitiges Zertifikat für die Option \"Mit Stirling-PDF signieren\" aktivieren"
[admin.settings.features.serverCertificate.organizationName] [admin.settings.features.serverCertificate.organizationName]
label = "Name der Organisation" label = "Name der Organisation"
@@ -5328,13 +5425,13 @@ confirmPrompt = "Sind Sie sicher, dass Sie fortfahren möchten?"
confirmCta = "Schlüssel aktualisieren" confirmCta = "Schlüssel aktualisieren"
[config.apiKeys.alert] [config.apiKeys.alert]
apiKeyErrorTitle = "API-Schlüssel-Fehler" apiKeyErrorTitle = "API Key Error"
failedToCreateApiKey = "API-Schlüssel konnte nicht erstellt werden." apiKeyRefreshed = "API Key Refreshed"
failedToRetrieveApiKey = "API-Schlüssel konnte nicht aus der Antwort abgerufen werden." apiKeyRefreshedBody = "Your API key has been successfully refreshed."
failedToFetchApiKey = "API-Schlüssel konnte nicht abgerufen werden." failedToCreateApiKey = "Failed to create API key."
apiKeyRefreshed = "API-Schlüssel aktualisiert" failedToFetchApiKey = "Failed to fetch API key."
apiKeyRefreshedBody = "Ihr API-Schlüssel wurde erfolgreich aktualisiert." failedToRefreshApiKey = "Failed to refresh API key."
failedToRefreshApiKey = "API-Schlüssel konnte nicht aktualisiert werden." failedToRetrieveApiKey = "Failed to retrieve API key from response."
[AddAttachmentsRequest] [AddAttachmentsRequest]
attachments = "Anhänge auswählen" attachments = "Anhänge auswählen"
@@ -5346,7 +5443,7 @@ selectedFiles = "Ausgewählte Dateien"
submit = "Anhänge hinzufügen" submit = "Anhänge hinzufügen"
[AddAttachmentsRequest.tooltip.header] [AddAttachmentsRequest.tooltip.header]
title = "Über Anhänge hinzufügen" title = "Über \"Anhänge hinzufügen\""
[AddAttachmentsRequest.tooltip.description] [AddAttachmentsRequest.tooltip.description]
title = "Funktion" title = "Funktion"
@@ -5355,7 +5452,7 @@ title = "Funktion"
title = "Anhangs-Ergebnisse" title = "Anhangs-Ergebnisse"
[AddAttachmentsRequest.error] [AddAttachmentsRequest.error]
failed = "Operation Anhänge hinzufügen fehlgeschlagen" failed = "Operation \"Anhänge hinzufügen\" fehlgeschlagen"
[addAttachments.error] [addAttachments.error]
failed = "Beim Hinzufügen von Anhängen zur PDF ist ein Fehler aufgetreten." failed = "Beim Hinzufügen von Anhängen zur PDF ist ein Fehler aufgetreten."
@@ -6473,7 +6570,6 @@ pen = "Pen"
polygon = "Polygon" polygon = "Polygon"
rectangle = "Rectangle" rectangle = "Rectangle"
redo = "Redo" redo = "Redo"
saveChanges = "Save Changes"
saveFailed = "Unable to save copy" saveFailed = "Unable to save copy"
saveReady = "Download ready" saveReady = "Download ready"
savingCopy = "Preparing download..." savingCopy = "Preparing download..."
@@ -6496,11 +6592,17 @@ title = "Annotate"
underline = "Underline" underline = "Underline"
undo = "Undo" undo = "Undo"
unsupportedType = "This annotation type is not fully supported for editing." unsupportedType = "This annotation type is not fully supported for editing."
saveChanges = "Save Changes"
[footer] [footer]
discord = "Discord" discord = "Discord"
issues = "GitHub" issues = "GitHub"
[textAlign]
center = "Center"
left = "Left"
right = "Right"
[mobileScanner] [mobileScanner]
addToBatch = "Add to Batch" addToBatch = "Add to Batch"
back = "Back" back = "Back"
@@ -6555,8 +6657,3 @@ pollingError = "Error checking for files"
sessionCreateError = "Failed to create session" sessionCreateError = "Failed to create session"
sessionId = "Session ID" sessionId = "Session ID"
title = "Upload from Mobile" title = "Upload from Mobile"
[textAlign]
center = "Center"
left = "Left"
right = "Right"
@@ -367,6 +367,7 @@ advanced = "Advanced"
title = "Security & Authentication" title = "Security & Authentication"
security = "Security" security = "Security"
connections = "Connections" connections = "Connections"
telegram = "Telegram"
[settings.licensingAnalytics] [settings.licensingAnalytics]
title = "Licensing & Analytics" title = "Licensing & Analytics"
@@ -4633,6 +4634,90 @@ description = "Automatically create user accounts on first SAML2 login"
label = "Block Registration" label = "Block Registration"
description = "Prevent new user registration via SAML2" description = "Prevent new user registration via SAML2"
[admin.settings.telegram]
title = "Telegram Bot"
description = "Configure Telegram bot connectivity, access controls, and feedback behavior."
[admin.settings.telegram.enabled]
label = "Enable Telegram Bot"
description = "Allow users to interact with Stirling PDF through your configured Telegram bot."
[admin.settings.telegram.botUsername]
label = "Bot Username"
description = "The public username of your Telegram bot."
[admin.settings.telegram.botToken]
label = "Bot Token"
description = "API token provided by BotFather for your Telegram bot."
[admin.settings.telegram.pipelineInboxFolder]
label = "Inbox Folder"
description = "Folder under the pipeline directory where incoming Telegram files are stored."
[admin.settings.telegram.customFolderSuffix]
label = "Use Custom Folder Suffix"
description = "Append the chat ID to incoming file folders to isolate uploads per chat."
[admin.settings.telegram.accessControl]
title = "Access Control"
description = "Restrict which users or channels can interact with the bot."
[admin.settings.telegram.enableAllowUserIDs]
label = "Allow Specific User IDs"
description = "When enabled, only listed user IDs can use the bot."
[admin.settings.telegram.allowUserIDs]
label = "Allowed User IDs"
description = "Enter Telegram user IDs allowed to interact with the bot."
placeholder = "Add user ID and press enter"
[admin.settings.telegram.enableAllowChannelIDs]
label = "Allow Specific Channel IDs"
description = "When enabled, only listed channel IDs can use the bot."
[admin.settings.telegram.allowChannelIDs]
label = "Allowed Channel IDs"
description = "Enter Telegram channel IDs allowed to interact with the bot."
placeholder = "Add channel ID and press enter"
[admin.settings.telegram.processing]
title = "Processing"
description = "Control polling intervals and processing timeouts for Telegram uploads."
[admin.settings.telegram.processingTimeoutSeconds]
label = "Processing Timeout (seconds)"
description = "Maximum time to wait for a processing job before reporting an error."
[admin.settings.telegram.pollingIntervalMillis]
label = "Polling Interval (ms)"
description = "Interval between checks for new Telegram updates."
[admin.settings.telegram.feedback]
title = "Feedback Messages"
description = "Choose when the bot should send feedback to users and channels."
[admin.settings.telegram.feedback.general.enabled]
label = "Enable Feedback"
description = "Control whether the bot sends feedback messages at all."
[admin.settings.telegram.feedback.channel]
title = "Channel Feedback Rules"
noValidDocument.label = "Show \"No valid document\" (Channel)"
noValidDocument.description = "Suppress the no valid document response for channel uploads."
errorProcessing.label = "Show processing errors (Channel)"
errorProcessing.description = "Send processing error messages to channels."
errorMessage.label = "Show error messages (Channel)"
errorMessage.description = "Show detailed error messages for channels."
[admin.settings.telegram.feedback.user]
title = "User Feedback Rules"
noValidDocument.label = "Show \"No valid document\" (User)"
noValidDocument.description = "Suppress the no valid document response for user uploads."
errorProcessing.label = "Show processing errors (User)"
errorProcessing.description = "Send processing error messages to users."
errorMessage.label = "Show error messages (User)"
errorMessage.description = "Show detailed error messages for users."
[admin.settings.connections.mobileScanner] [admin.settings.connections.mobileScanner]
label = "Mobile Phone Upload" label = "Mobile Phone Upload"
enable = "Enable QR Code Upload" enable = "Enable QR Code Upload"
@@ -1,5 +1,18 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Paper, Group, Text, Button, Collapse, Stack, TextInput, Textarea, Switch, PasswordInput } from '@mantine/core'; import {
Paper,
Group,
Text,
Button,
Collapse,
Stack,
TextInput,
Textarea,
Switch,
PasswordInput,
NumberInput,
TagsInput,
} from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import LocalIcon from '@app/components/shared/LocalIcon'; import LocalIcon from '@app/components/shared/LocalIcon';
import { Provider, ProviderField } from '@app/components/shared/config/configSections/providerDefinitions'; import { Provider, ProviderField } from '@app/components/shared/config/configSections/providerDefinitions';
@@ -105,6 +118,36 @@ export default function ProviderCard({
/> />
); );
case 'number':
return (
<NumberInput
key={field.key}
label={field.label}
description={field.description}
placeholder={field.placeholder}
value={value}
onChange={(num) => handleFieldChange(field.key, num)}
disabled={disabled}
allowDecimal={false}
/>
);
case 'tags': {
const tagValue = Array.isArray(value) ? value.map((val) => `${val}`) : [];
return (
<TagsInput
key={field.key}
label={field.label}
description={field.description}
placeholder={field.placeholder}
value={tagValue}
onChange={(vals) => handleFieldChange(field.key, vals)}
disabled={disabled}
/>
);
}
default: default:
return ( return (
<TextInput <TextInput
@@ -1,8 +1,10 @@
export type ProviderType = 'oauth2' | 'saml2'; import { useTranslation } from 'react-i18next';
export type ProviderType = 'oauth2' | 'saml2' | 'telegram';
export interface ProviderField { export interface ProviderField {
key: string; key: string;
type: 'text' | 'password' | 'switch' | 'textarea'; type: 'text' | 'password' | 'switch' | 'textarea' | 'number' | 'tags';
label: string; label: string;
description: string; description: string;
placeholder?: string; placeholder?: string;
@@ -231,7 +233,7 @@ export const SMTP_PROVIDER: Provider = {
}, },
{ {
key: 'port', key: 'port',
type: 'text', type: 'number',
label: 'SMTP Port', label: 'SMTP Port',
description: 'The port number for SMTP connection (typically 25, 465, or 587)', description: 'The port number for SMTP connection (typically 25, 465, or 587)',
placeholder: '587', placeholder: '587',
@@ -258,6 +260,216 @@ export const SMTP_PROVIDER: Provider = {
}, },
], ],
}; };
const useTelegramProvider = (): Provider => {
const { t } = useTranslation();
return {
id: 'telegram',
name: t('admin.settings.telegram.title', 'Telegram Bot'),
icon: 'send-rounded',
type: 'telegram',
scope: t(
'admin.settings.telegram.description',
'Configure Telegram bot connectivity, access controls, and feedback behavior.'
),
fields: [
{
key: 'enabled',
type: 'switch',
label: t('admin.settings.telegram.enabled.label', 'Enable Telegram Bot'),
description: t(
'admin.settings.telegram.enabled.description',
'Allow users to interact with Stirling PDF through your configured Telegram bot.'
),
defaultValue: false,
},
{
key: 'botUsername',
type: 'text',
label: t('admin.settings.telegram.botUsername.label', 'Bot Username'),
description: t(
'admin.settings.telegram.botUsername.description',
'The public username of your Telegram bot.'
),
placeholder: 'my_pdf_bot',
},
{
key: 'botToken',
type: 'password',
label: t('admin.settings.telegram.botToken.label', 'Bot Token'),
description: t(
'admin.settings.telegram.botToken.description',
'API token provided by BotFather for your Telegram bot.'
),
placeholder: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11',
},
{
key: 'pipelineInboxFolder',
type: 'text',
label: t('admin.settings.telegram.pipelineInboxFolder.label', 'Inbox Folder'),
description: t(
'admin.settings.telegram.pipelineInboxFolder.description',
'Folder under the pipeline directory where incoming Telegram files are stored.'
),
placeholder: 'telegram',
},
{
key: 'customFolderSuffix',
type: 'switch',
label: t('admin.settings.telegram.customFolderSuffix.label', 'Use Custom Folder Suffix'),
description: t(
'admin.settings.telegram.customFolderSuffix.description',
'Append the chat ID to incoming file folders to isolate uploads per chat.'
),
defaultValue: false,
},
{
key: 'enableAllowUserIDs',
type: 'switch',
label: t('admin.settings.telegram.enableAllowUserIDs.label', 'Allow Specific User IDs'),
description: t(
'admin.settings.telegram.enableAllowUserIDs.description',
'When enabled, only listed user IDs can use the bot.'
),
defaultValue: false,
},
{
key: 'allowUserIDs',
type: 'tags',
label: t('admin.settings.telegram.allowUserIDs.label', 'Allowed User IDs'),
description: t(
'admin.settings.telegram.allowUserIDs.description',
'Enter Telegram user IDs allowed to interact with the bot.'
),
placeholder: t('admin.settings.telegram.allowUserIDs.placeholder', 'Add user ID and press enter'),
defaultValue: [],
},
{
key: 'enableAllowChannelIDs',
type: 'switch',
label: t('admin.settings.telegram.enableAllowChannelIDs.label', 'Allow Specific Channel IDs'),
description: t(
'admin.settings.telegram.enableAllowChannelIDs.description',
'When enabled, only listed channel IDs can use the bot.'
),
defaultValue: false,
},
{
key: 'allowChannelIDs',
type: 'tags',
label: t('admin.settings.telegram.allowChannelIDs.label', 'Allowed Channel IDs'),
description: t(
'admin.settings.telegram.allowChannelIDs.description',
'Enter Telegram channel IDs allowed to interact with the bot.'
),
placeholder: t('admin.settings.telegram.allowChannelIDs.placeholder', 'Add channel ID and press enter'),
defaultValue: [],
},
{
key: 'processingTimeoutSeconds',
type: 'number',
label: t(
'admin.settings.telegram.processingTimeoutSeconds.label',
'Processing Timeout (seconds)'
),
description: t(
'admin.settings.telegram.processingTimeoutSeconds.description',
'Maximum time to wait for a processing job before reporting an error.'
),
defaultValue: 180,
},
{
key: 'pollingIntervalMillis',
type: 'number',
label: t('admin.settings.telegram.pollingIntervalMillis.label', 'Polling Interval (ms)'),
description: t(
'admin.settings.telegram.pollingIntervalMillis.description',
'Interval between checks for new Telegram updates.'
),
defaultValue: 2000,
},
{
key: 'feedback.general.enabled',
type: 'switch',
label: t('admin.settings.telegram.feedback.general.enabled.label', 'Enable Feedback'),
description: t(
'admin.settings.telegram.feedback.general.enabled.description',
'Control whether the bot sends feedback messages at all.'
),
defaultValue: true,
},
{
key: 'feedback.channel.noValidDocument',
type: 'switch',
label: t(
'admin.settings.telegram.feedback.channel.noValidDocument.label',
'Show "No valid document" (Channel)'
),
description: t(
'admin.settings.telegram.feedback.channel.noValidDocument.description',
'Suppress the no valid document response for channel uploads.'
),
defaultValue: false,
},
{
key: 'feedback.channel.errorProcessing',
type: 'switch',
label: t(
'admin.settings.telegram.feedback.channel.errorProcessing.label',
'Show processing errors (Channel)'
),
description: t(
'admin.settings.telegram.feedback.channel.errorProcessing.description',
'Send processing error messages to channels.'
),
defaultValue: false,
},
{
key: 'feedback.channel.errorMessage',
type: 'switch',
label: t(
'admin.settings.telegram.feedback.channel.errorMessage.label',
'Show error messages (Channel)'
),
description: t(
'admin.settings.telegram.feedback.channel.errorMessage.description',
'Show detailed error messages for channels.'
),
defaultValue: false,
},
{
key: 'feedback.user.noValidDocument',
type: 'switch',
label: t('admin.settings.telegram.feedback.user.noValidDocument.label', 'Show "No valid document" (User)'),
description: t(
'admin.settings.telegram.feedback.user.noValidDocument.description',
'Suppress the no valid document response for user uploads.'
),
defaultValue: false,
},
{
key: 'feedback.user.errorProcessing',
type: 'switch',
label: t('admin.settings.telegram.feedback.user.errorProcessing.label', 'Show processing errors (User)'),
description: t(
'admin.settings.telegram.feedback.user.errorProcessing.description',
'Send processing error messages to users.'
),
defaultValue: false,
},
{
key: 'feedback.user.errorMessage',
type: 'switch',
label: t('admin.settings.telegram.feedback.user.errorMessage.label', 'Show error messages (User)'),
description: t(
'admin.settings.telegram.feedback.user.errorMessage.description',
'Show detailed error messages for users.'
),
defaultValue: false,
},
],
};
};
export const SAML2_PROVIDER: Provider = { export const SAML2_PROVIDER: Provider = {
id: 'saml2', id: 'saml2',
@@ -352,4 +564,14 @@ export const SAML2_PROVIDER: Provider = {
], ],
}; };
export const ALL_PROVIDERS = [...OAUTH2_PROVIDERS, GENERIC_OAUTH2_PROVIDER, SAML2_PROVIDER, SMTP_PROVIDER]; export const useAllProviders = (): Provider[] => {
const telegramProvider = useTelegramProvider();
return [
...OAUTH2_PROVIDERS,
GENERIC_OAUTH2_PROVIDER,
SAML2_PROVIDER,
SMTP_PROVIDER,
telegramProvider,
];
};
@@ -18,6 +18,7 @@ export const VALID_NAV_KEYS = [
'adminGeneral', 'adminGeneral',
'adminSecurity', 'adminSecurity',
'adminConnections', 'adminConnections',
'adminTelegram',
'adminPrivacy', 'adminPrivacy',
'adminDatabase', 'adminDatabase',
'adminAdvanced', 'adminAdvanced',
@@ -10,14 +10,38 @@ import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge'; import PendingBadge from '@app/components/shared/config/PendingBadge';
import { Z_INDEX_CONFIG_MODAL } from '@app/styles/zIndex'; import { Z_INDEX_CONFIG_MODAL } from '@app/styles/zIndex';
import ProviderCard from '@app/components/shared/config/configSections/ProviderCard'; import ProviderCard from '@app/components/shared/config/configSections/ProviderCard';
import { import { Provider, useAllProviders } from '@app/components/shared/config/configSections/providerDefinitions';
ALL_PROVIDERS,
Provider,
} from '@app/components/shared/config/configSections/providerDefinitions';
import apiClient from '@app/services/apiClient'; import apiClient from '@app/services/apiClient';
import { useLoginRequired } from '@app/hooks/useLoginRequired'; import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner'; import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
interface FeedbackFlags {
noValidDocument?: boolean;
errorProcessing?: boolean;
errorMessage?: boolean;
}
interface FeedbackSettings {
general?: { enabled?: boolean };
channel?: FeedbackFlags;
user?: FeedbackFlags;
}
interface TelegramSettingsData {
enabled?: boolean;
botToken?: string;
botUsername?: string;
pipelineInboxFolder?: string;
customFolderSuffix?: boolean;
enableAllowUserIDs?: boolean;
allowUserIDs?: number[];
enableAllowChannelIDs?: boolean;
allowChannelIDs?: number[];
processingTimeoutSeconds?: number;
pollingIntervalMillis?: number;
feedback?: FeedbackSettings;
}
interface ConnectionsSettingsData { interface ConnectionsSettingsData {
oauth2?: { oauth2?: {
enabled?: boolean; enabled?: boolean;
@@ -45,6 +69,7 @@ interface ConnectionsSettingsData {
password?: string; password?: string;
from?: string; from?: string;
}; };
telegram?: TelegramSettingsData;
ssoAutoLogin?: boolean; ssoAutoLogin?: boolean;
enableMobileScanner?: boolean; enableMobileScanner?: boolean;
mobileScannerConvertToPdf?: boolean; mobileScannerConvertToPdf?: boolean;
@@ -58,6 +83,7 @@ export default function AdminConnectionsSection() {
const navigate = useNavigate(); const navigate = useNavigate();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired(); const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const allProviders = useAllProviders();
const adminSettings = useAdminSettings<ConnectionsSettingsData>({ const adminSettings = useAdminSettings<ConnectionsSettingsData>({
sectionName: 'connections', sectionName: 'connections',
@@ -74,6 +100,10 @@ export default function AdminConnectionsSection() {
const premiumResponse = await apiClient.get('/api/v1/admin/settings/section/premium'); const premiumResponse = await apiClient.get('/api/v1/admin/settings/section/premium');
const premiumData = premiumResponse.data || {}; const premiumData = premiumResponse.data || {};
// Fetch Telegram settings
const telegramResponse = await apiClient.get('/api/v1/admin/settings/section/telegram');
const telegramData = telegramResponse.data || {};
// Fetch system settings for enableMobileScanner // Fetch system settings for enableMobileScanner
const systemResponse = await apiClient.get('/api/v1/admin/settings/section/system'); const systemResponse = await apiClient.get('/api/v1/admin/settings/section/system');
const systemData = systemResponse.data || {}; const systemData = systemResponse.data || {};
@@ -82,6 +112,7 @@ export default function AdminConnectionsSection() {
oauth2: securityData.oauth2 || {}, oauth2: securityData.oauth2 || {},
saml2: securityData.saml2 || {}, saml2: securityData.saml2 || {},
mail: mailData || {}, mail: mailData || {},
telegram: telegramData || {},
ssoAutoLogin: premiumData.proFeatures?.ssoAutoLogin || false, ssoAutoLogin: premiumData.proFeatures?.ssoAutoLogin || false,
enableMobileScanner: systemData.enableMobileScanner || false, enableMobileScanner: systemData.enableMobileScanner || false,
mobileScannerConvertToPdf: systemData.mobileScannerSettings?.convertToPdf !== false, mobileScannerConvertToPdf: systemData.mobileScannerSettings?.convertToPdf !== false,
@@ -101,6 +132,9 @@ export default function AdminConnectionsSection() {
if (mailData._pending) { if (mailData._pending) {
pendingBlock.mail = mailData._pending; pendingBlock.mail = mailData._pending;
} }
if (telegramData._pending) {
pendingBlock.telegram = telegramData._pending;
}
if (premiumData._pending?.proFeatures?.ssoAutoLogin !== undefined) { if (premiumData._pending?.proFeatures?.ssoAutoLogin !== undefined) {
pendingBlock.ssoAutoLogin = premiumData._pending.proFeatures.ssoAutoLogin; pendingBlock.ssoAutoLogin = premiumData._pending.proFeatures.ssoAutoLogin;
} }
@@ -162,6 +196,10 @@ export default function AdminConnectionsSection() {
return settings?.mail?.enabled === true; return settings?.mail?.enabled === true;
} }
if (provider.id === 'telegram') {
return settings?.telegram?.enabled === true;
}
if (provider.id === 'oauth2-generic') { if (provider.id === 'oauth2-generic') {
return settings?.oauth2?.enabled === true; return settings?.oauth2?.enabled === true;
} }
@@ -180,6 +218,10 @@ export default function AdminConnectionsSection() {
return settings?.mail || {}; return settings?.mail || {};
} }
if (provider.id === 'telegram') {
return settings?.telegram || {};
}
if (provider.id === 'oauth2-generic') { if (provider.id === 'oauth2-generic') {
// Generic OAuth2 settings are at the root oauth2 level // Generic OAuth2 settings are at the root oauth2 level
return { return {
@@ -210,6 +252,35 @@ export default function AdminConnectionsSection() {
// Mail settings use a different endpoint // Mail settings use a different endpoint
const response = await apiClient.put('/api/v1/admin/settings/section/mail', providerSettings); const response = await apiClient.put('/api/v1/admin/settings/section/mail', providerSettings);
if (response.status === 200) {
await fetchSettings(); // Refresh settings
alert({
alertType: 'success',
title: t('admin.success', 'Success'),
body: t('admin.settings.saveSuccess', 'Settings saved successfully'),
});
showRestartModal();
} else {
throw new Error('Failed to save');
}
} else if (provider.id === 'telegram') {
const parseToNumberArray = (values: any) =>
(Array.isArray(values) ? values : [])
.map((value) => Number(value))
.filter((value) => !Number.isNaN(value));
const response = await apiClient.put('/api/v1/admin/settings/section/telegram', {
...providerSettings,
allowUserIDs: parseToNumberArray(providerSettings.allowUserIDs),
allowChannelIDs: parseToNumberArray(providerSettings.allowChannelIDs),
processingTimeoutSeconds: providerSettings.processingTimeoutSeconds
? Number(providerSettings.processingTimeoutSeconds)
: undefined,
pollingIntervalMillis: providerSettings.pollingIntervalMillis
? Number(providerSettings.pollingIntervalMillis)
: undefined,
});
if (response.status === 200) { if (response.status === 200) {
await fetchSettings(); // Refresh settings await fetchSettings(); // Refresh settings
alert({ alert({
@@ -271,11 +342,27 @@ export default function AdminConnectionsSection() {
return; return;
} }
try{ try {
if (provider.id === 'smtp') { if (provider.id === 'smtp') {
// Mail settings use a different endpoint // Mail settings use a different endpoint
const response = await apiClient.put('/api/v1/admin/settings/section/mail', { enabled: false }); const response = await apiClient.put('/api/v1/admin/settings/section/mail', { enabled: false });
if (response.status === 200) {
await fetchSettings();
alert({
alertType: 'success',
title: t('admin.success', 'Success'),
body: t('admin.settings.connections.disconnected', 'Provider disconnected successfully'),
});
showRestartModal();
} else {
throw new Error('Failed to disconnect');
}
} else if (provider.id === 'telegram') {
const response = await apiClient.put('/api/v1/admin/settings/section/telegram', {
enabled: false,
});
if (response.status === 200) { if (response.status === 200) {
await fetchSettings(); await fetchSettings();
alert({ alert({
@@ -425,8 +512,8 @@ export default function AdminConnectionsSection() {
} }
}; };
const linkedProviders = ALL_PROVIDERS.filter((p) => isProviderConfigured(p)); const linkedProviders = allProviders.filter((p) => isProviderConfigured(p));
const availableProviders = ALL_PROVIDERS.filter((p) => !isProviderConfigured(p)); const availableProviders = allProviders.filter((p) => !isProviderConfigured(p));
return ( return (
<Stack gap="xl"> <Stack gap="xl">