Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/api/settings/telegram/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const TELEGRAM_KEYS = [
'telegram_notify_error',
'telegram_notify_permission',
'telegram_bridge_allowed_users',
'telegram_proxy_url',
] as const;

export async function GET() {
Expand Down
39 changes: 39 additions & 0 deletions src/components/bridge/TelegramBridgeSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,23 @@ interface TelegramBridgeSettings {
telegram_bot_token: string;
telegram_chat_id: string;
telegram_bridge_allowed_users: string;
telegram_proxy_url: string;
}

const DEFAULT_SETTINGS: TelegramBridgeSettings = {
telegram_bot_token: "",
telegram_chat_id: "",
telegram_bridge_allowed_users: "",
telegram_proxy_url: "",
};

export function TelegramBridgeSection() {
const [, setSettings] = useState<TelegramBridgeSettings>(DEFAULT_SETTINGS);
const [botToken, setBotToken] = useState("");
const [chatId, setChatId] = useState("");
const [allowedUsers, setAllowedUsers] = useState("");
const [proxyUrl, setProxyUrl] = useState("");
const [proxyUrlError, setProxyUrlError] = useState("");
const [saving, setSaving] = useState(false);
const [verifying, setVerifying] = useState(false);
const [detecting, setDetecting] = useState(false);
Expand All @@ -44,6 +48,7 @@ export function TelegramBridgeSection() {
setBotToken(s.telegram_bot_token);
setChatId(s.telegram_chat_id);
setAllowedUsers(s.telegram_bridge_allowed_users);
setProxyUrl(s.telegram_proxy_url);
}
} catch {
// ignore
Expand Down Expand Up @@ -73,12 +78,27 @@ export function TelegramBridgeSection() {
};

const handleSaveCredentials = () => {
// Validate proxy URL if provided
if (proxyUrl) {
try {
const parsed = new URL(proxyUrl);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
setProxyUrlError(t("telegram.proxyInvalidProtocol"));
return;
}
} catch {
setProxyUrlError(t("telegram.proxyInvalidUrl"));
return;
}
}
setProxyUrlError("");
const updates: Partial<TelegramBridgeSettings> = {};
if (botToken && !botToken.startsWith("***")) {
updates.telegram_bot_token = botToken;
}
updates.telegram_chat_id = chatId;
updates.telegram_bridge_allowed_users = allowedUsers;
updates.telegram_proxy_url = proxyUrl;
saveSettings(updates);
};

Expand Down Expand Up @@ -223,6 +243,25 @@ export function TelegramBridgeSection() {
{t("telegram.chatIdHint")}
</p>
</div>

<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">
{t("telegram.proxy")}
</label>
<Input
value={proxyUrl}
onChange={(e) => { setProxyUrl(e.target.value); setProxyUrlError(""); }}
placeholder={t("telegram.proxyPlaceholder")}
className={`font-mono text-sm${proxyUrlError ? " border-destructive" : ""}`}
/>
{proxyUrlError ? (
<p className="text-xs text-destructive mt-1">{proxyUrlError}</p>
) : (
<p className="text-xs text-muted-foreground mt-1">
{t("telegram.proxyHint")}
</p>
)}
</div>
</div>

<div className="flex items-center gap-2">
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,12 @@ const en = {
'telegram.step4': 'Click "Test Connection" to verify the token is valid',
'telegram.step5': 'Send /start to your bot, then click "Auto Detect" next to the Chat ID field',
'telegram.step6': 'Click "Save" to store your credentials',
'telegram.proxy': 'Proxy Server',
'telegram.proxyDesc': 'Optional HTTP/HTTPS proxy for Telegram API requests (useful in regions where Telegram is blocked)',
'telegram.proxyPlaceholder': 'http://127.0.0.1:7890',
'telegram.proxyHint': 'Leave blank to connect directly. Supports http:// and https:// proxy URLs.',
'telegram.proxyInvalidUrl': 'Invalid proxy URL. Example: http://127.0.0.1:7890',
'telegram.proxyInvalidProtocol': 'Only http:// and https:// proxy URLs are supported.',

// ── Feishu (Bridge) ──────────────────────────────────────
'feishu.credentials': 'App Credentials',
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,12 @@ const zh: Record<TranslationKey, string> = {
'telegram.step4': '点击「测试连接」验证 Token 是否有效',
'telegram.step5': '向您的 Bot 发送 /start,然后点击 Chat ID 旁的「自动检测」按钮',
'telegram.step6': '点击「保存」存储凭据',
'telegram.proxy': '代理服务器',
'telegram.proxyDesc': '可选的 HTTP/HTTPS 代理,用于 Telegram API 请求(在 Telegram 被封锁的地区很有用)',
'telegram.proxyPlaceholder': 'http://127.0.0.1:7890',
'telegram.proxyHint': '留空则直接连接。支持 http:// 和 https:// 代理地址。',
'telegram.proxyInvalidUrl': '代理地址无效,示例:http://127.0.0.1:7890',
'telegram.proxyInvalidProtocol': '仅支持 http:// 和 https:// 代理地址。',

// ── Feishu (Bridge) ──────────────────────────────────────
'feishu.credentials': '应用凭据',
Expand Down
8 changes: 5 additions & 3 deletions src/lib/bridge/adapters/telegram-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
} from '../types';
import type { FileAttachment } from '@/types';
import { BaseChannelAdapter, registerAdapterFactory } from '../channel-adapter';
import { callTelegramApi, sendMessageDraft } from './telegram-utils';
import { callTelegramApi, sendMessageDraft, proxyFetchOptions } from './telegram-utils';
import {
isImageEnabled,
downloadPhoto,
Expand Down Expand Up @@ -394,7 +394,8 @@ export class TelegramAdapter extends BaseChannelAdapter {
const res = await fetch(url, {
method: 'GET',
signal: AbortSignal.timeout(10_000),
});
...proxyFetchOptions(),
} as RequestInit);
const data = await res.json();
if (data.ok && data.result?.id) {
this.botUserId = String(data.result.id);
Expand Down Expand Up @@ -484,7 +485,8 @@ export class TelegramAdapter extends BaseChannelAdapter {
allowed_updates: ['message', 'callback_query'],
}),
signal: this.abortController?.signal,
});
...proxyFetchOptions(),
} as RequestInit);

if (!this.running) break;

Expand Down
7 changes: 5 additions & 2 deletions src/lib/bridge/adapters/telegram-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import type { FileAttachment } from '@/types';
import { getSetting } from '../../db';
import { proxyFetchOptions } from './telegram-utils';

const TELEGRAM_API = 'https://api.telegram.org';

Expand Down Expand Up @@ -206,7 +207,8 @@ async function downloadFileById(
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_id: fileId }),
signal: AbortSignal.timeout(15_000),
});
...proxyFetchOptions(),
} as RequestInit);

const getFileData = await getFileRes.json();
if (!getFileData.ok || !getFileData.result?.file_path) {
Expand All @@ -231,7 +233,8 @@ async function downloadFileById(
const downloadUrl = `${TELEGRAM_API}/file/bot${botToken}/${filePath}`;
const downloadRes = await fetch(downloadUrl, {
signal: AbortSignal.timeout(60_000),
});
...proxyFetchOptions(),
} as RequestInit);

if (!downloadRes.ok) {
console.warn(`[telegram-media] Download failed: HTTP ${downloadRes.status}`);
Expand Down
52 changes: 51 additions & 1 deletion src/lib/bridge/adapters/telegram-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,57 @@
* Extracted from telegram-bot.ts to avoid duplication.
*/

import { getSetting } from '../../db';

const TELEGRAM_API = 'https://api.telegram.org';

// ── Proxy Support ─────────────────────────────────────────────

/**
* Read the configured Telegram proxy URL from settings.
* Returns an empty string when no proxy is configured.
*/
export function getTelegramProxyUrl(): string {
return getSetting('telegram_proxy_url') || '';
}

/**
* Build extra fetch init options to route requests through the configured
* HTTP/HTTPS proxy (if any). Uses undici's ProxyAgent which is bundled
* with Node.js 22+ — no additional npm dependency required.
*
* Returns an empty object when no proxy is set, the URL is invalid, or
* when undici is unavailable.
*/
export function proxyFetchOptions(): Record<string, unknown> {
const proxyUrl = getTelegramProxyUrl();
if (!proxyUrl) return {};

// Validate that the proxy URL uses a supported protocol
let parsed: URL;
try {
parsed = new URL(proxyUrl);
} catch {
console.warn('[telegram] Invalid proxy URL (cannot parse), connecting without proxy:', proxyUrl);
return {};
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
console.warn('[telegram] Unsupported proxy protocol (only http/https supported):', parsed.protocol);
return {};
}

try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { ProxyAgent } = require('undici') as {
ProxyAgent: new (url: string) => { [Symbol.toStringTag]: string };
};
return { dispatcher: new ProxyAgent(proxyUrl) };
} catch {
console.warn('[telegram] undici ProxyAgent unavailable, connecting without proxy');
return {};
}
}

export interface TelegramSendResult {
ok: boolean;
messageId?: string;
Expand Down Expand Up @@ -45,7 +94,8 @@ export async function callTelegramApi(
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
...proxyFetchOptions(),
} as RequestInit);
const httpStatus = res.status;
const data: TelegramApiResponse = await res.json();
if (!data.ok) {
Expand Down