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
@@ -10,14 +10,38 @@ import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge';
import { Z_INDEX_CONFIG_MODAL } from '@app/styles/zIndex';
import ProviderCard from '@app/components/shared/config/configSections/ProviderCard';
import {
ALL_PROVIDERS,
Provider,
} from '@app/components/shared/config/configSections/providerDefinitions';
import { Provider, useAllProviders } from '@app/components/shared/config/configSections/providerDefinitions';
import apiClient from '@app/services/apiClient';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
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 {
oauth2?: {
enabled?: boolean;
@@ -45,6 +69,7 @@ interface ConnectionsSettingsData {
password?: string;
from?: string;
};
telegram?: TelegramSettingsData;
ssoAutoLogin?: boolean;
enableMobileScanner?: boolean;
mobileScannerConvertToPdf?: boolean;
@@ -58,6 +83,7 @@ export default function AdminConnectionsSection() {
const navigate = useNavigate();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } = useLoginRequired();
const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer();
const allProviders = useAllProviders();
const adminSettings = useAdminSettings<ConnectionsSettingsData>({
sectionName: 'connections',
@@ -74,6 +100,10 @@ export default function AdminConnectionsSection() {
const premiumResponse = await apiClient.get('/api/v1/admin/settings/section/premium');
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
const systemResponse = await apiClient.get('/api/v1/admin/settings/section/system');
const systemData = systemResponse.data || {};
@@ -82,6 +112,7 @@ export default function AdminConnectionsSection() {
oauth2: securityData.oauth2 || {},
saml2: securityData.saml2 || {},
mail: mailData || {},
telegram: telegramData || {},
ssoAutoLogin: premiumData.proFeatures?.ssoAutoLogin || false,
enableMobileScanner: systemData.enableMobileScanner || false,
mobileScannerConvertToPdf: systemData.mobileScannerSettings?.convertToPdf !== false,
@@ -101,6 +132,9 @@ export default function AdminConnectionsSection() {
if (mailData._pending) {
pendingBlock.mail = mailData._pending;
}
if (telegramData._pending) {
pendingBlock.telegram = telegramData._pending;
}
if (premiumData._pending?.proFeatures?.ssoAutoLogin !== undefined) {
pendingBlock.ssoAutoLogin = premiumData._pending.proFeatures.ssoAutoLogin;
}
@@ -162,6 +196,10 @@ export default function AdminConnectionsSection() {
return settings?.mail?.enabled === true;
}
if (provider.id === 'telegram') {
return settings?.telegram?.enabled === true;
}
if (provider.id === 'oauth2-generic') {
return settings?.oauth2?.enabled === true;
}
@@ -180,6 +218,10 @@ export default function AdminConnectionsSection() {
return settings?.mail || {};
}
if (provider.id === 'telegram') {
return settings?.telegram || {};
}
if (provider.id === 'oauth2-generic') {
// Generic OAuth2 settings are at the root oauth2 level
return {
@@ -210,6 +252,35 @@ export default function AdminConnectionsSection() {
// Mail settings use a different endpoint
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) {
await fetchSettings(); // Refresh settings
alert({
@@ -271,11 +342,27 @@ export default function AdminConnectionsSection() {
return;
}
try{
try {
if (provider.id === 'smtp') {
// Mail settings use a different endpoint
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) {
await fetchSettings();
alert({
@@ -425,8 +512,8 @@ export default function AdminConnectionsSection() {
}
};
const linkedProviders = ALL_PROVIDERS.filter((p) => isProviderConfigured(p));
const availableProviders = ALL_PROVIDERS.filter((p) => !isProviderConfigured(p));
const linkedProviders = allProviders.filter((p) => isProviderConfigured(p));
const availableProviders = allProviders.filter((p) => !isProviderConfigured(p));
return (
<Stack gap="xl">