Skip to content

Commit d635d4b

Browse files
committed
fix(telegram): accept URL/file_id inputs for send media
The Telegram send photo/video/audio/animation block was normalizing inputs as file-only and rejecting valid URL/file_id strings. Add a dedicated media normalizer that supports URLs, file_ids, UserFile objects, and stringified JSON references. Fixes #3220
1 parent 4913799 commit d635d4b

File tree

8 files changed

+240
-28
lines changed

8 files changed

+240
-28
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { TelegramBlock } from '@/blocks/blocks/telegram'
6+
7+
describe('TelegramBlock tools.config.params', () => {
8+
it('accepts public photo URLs for telegram_send_photo', () => {
9+
const params = TelegramBlock.tools.config.params({
10+
operation: 'telegram_send_photo',
11+
botToken: 'token',
12+
chatId: ' 123 ',
13+
photo: ' https://example.com/a.jpg ',
14+
caption: 'hello',
15+
} as any)
16+
17+
expect(params).toEqual({
18+
botToken: 'token',
19+
chatId: '123',
20+
photo: 'https://example.com/a.jpg',
21+
caption: 'hello',
22+
})
23+
})
24+
25+
it('accepts stringified JSON photo objects from advanced-mode references', () => {
26+
const params = TelegramBlock.tools.config.params({
27+
operation: 'telegram_send_photo',
28+
botToken: 'token',
29+
chatId: '123',
30+
photo: '{"url":"https://example.com/a.jpg"}',
31+
} as any)
32+
33+
expect(params.photo).toBe('https://example.com/a.jpg')
34+
})
35+
36+
it('supports legacy `withPhoto` alias', () => {
37+
const params = TelegramBlock.tools.config.params({
38+
operation: 'telegram_send_photo',
39+
botToken: 'token',
40+
chatId: '123',
41+
withPhoto: 'https://example.com/a.jpg',
42+
} as any)
43+
44+
expect(params.photo).toBe('https://example.com/a.jpg')
45+
})
46+
47+
it('rejects multiple photo values', () => {
48+
expect(() =>
49+
TelegramBlock.tools.config.params({
50+
operation: 'telegram_send_photo',
51+
botToken: 'token',
52+
chatId: '123',
53+
photo: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
54+
} as any)
55+
).toThrow('Photo reference must be a single item, not an array.')
56+
})
57+
})

apps/sim/blocks/blocks/telegram.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { TelegramIcon } from '@/components/icons'
22
import type { BlockConfig } from '@/blocks/types'
33
import { AuthMode } from '@/blocks/types'
44
import { normalizeFileInput } from '@/blocks/utils'
5+
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
56
import type { TelegramResponse } from '@/tools/telegram/types'
67
import { getTrigger } from '@/triggers'
78

@@ -269,55 +270,54 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
269270
messageId: params.messageId,
270271
}
271272
case 'telegram_send_photo': {
272-
// photo is the canonical param for both basic (photoFile) and advanced modes
273-
const photoSource = normalizeFileInput(params.photo, {
274-
single: true,
273+
// photo supports both public URLs/file_ids and UserFile objects.
274+
// Backwards-compatible aliases (e.g., `withPhoto`) are supported for older saved workflows.
275+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
276+
const rawPhoto = params.photo ?? (params as any).withPhoto ?? (params as any).with_photo
277+
const photoSource = normalizeTelegramMediaParam(rawPhoto, {
278+
label: 'Photo',
279+
errorMessage: 'Photo is required.',
275280
})
276-
if (!photoSource) {
277-
throw new Error('Photo is required.')
278-
}
279281
return {
280282
...commonParams,
281283
photo: photoSource,
282284
caption: params.caption,
283285
}
284286
}
285287
case 'telegram_send_video': {
286-
// video is the canonical param for both basic (videoFile) and advanced modes
287-
const videoSource = normalizeFileInput(params.video, {
288-
single: true,
288+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
289+
const rawVideo = params.video ?? (params as any).withVideo ?? (params as any).with_video
290+
const videoSource = normalizeTelegramMediaParam(rawVideo, {
291+
label: 'Video',
292+
errorMessage: 'Video is required.',
289293
})
290-
if (!videoSource) {
291-
throw new Error('Video is required.')
292-
}
293294
return {
294295
...commonParams,
295296
video: videoSource,
296297
caption: params.caption,
297298
}
298299
}
299300
case 'telegram_send_audio': {
300-
// audio is the canonical param for both basic (audioFile) and advanced modes
301-
const audioSource = normalizeFileInput(params.audio, {
302-
single: true,
301+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
302+
const rawAudio = params.audio ?? (params as any).withAudio ?? (params as any).with_audio
303+
const audioSource = normalizeTelegramMediaParam(rawAudio, {
304+
label: 'Audio',
305+
errorMessage: 'Audio is required.',
303306
})
304-
if (!audioSource) {
305-
throw new Error('Audio is required.')
306-
}
307307
return {
308308
...commonParams,
309309
audio: audioSource,
310310
caption: params.caption,
311311
}
312312
}
313313
case 'telegram_send_animation': {
314-
// animation is the canonical param for both basic (animationFile) and advanced modes
315-
const animationSource = normalizeFileInput(params.animation, {
316-
single: true,
314+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
315+
const rawAnimation =
316+
params.animation ?? (params as any).withAnimation ?? (params as any).with_animation
317+
const animationSource = normalizeTelegramMediaParam(rawAnimation, {
318+
label: 'Animation',
319+
errorMessage: 'Animation is required.',
317320
})
318-
if (!animationSource) {
319-
throw new Error('Animation is required.')
320-
}
321321
return {
322322
...commonParams,
323323
animation: animationSource,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
6+
7+
describe('normalizeTelegramMediaParam', () => {
8+
it('accepts trimmed URL/file_id strings', () => {
9+
expect(normalizeTelegramMediaParam(' https://example.com/a.jpg ', { label: 'Photo' })).toBe(
10+
'https://example.com/a.jpg'
11+
)
12+
expect(normalizeTelegramMediaParam(' ABC123 ', { label: 'Photo' })).toBe('ABC123')
13+
})
14+
15+
it('accepts URL instances', () => {
16+
expect(
17+
normalizeTelegramMediaParam(new URL('https://example.com/a.jpg'), { label: 'Photo' })
18+
).toBe('https://example.com/a.jpg')
19+
})
20+
21+
it('accepts object shapes with url/href/file_id', () => {
22+
expect(
23+
normalizeTelegramMediaParam({ url: 'https://example.com/a.jpg' }, { label: 'Photo' })
24+
).toBe('https://example.com/a.jpg')
25+
expect(
26+
normalizeTelegramMediaParam({ href: 'https://example.com/a.jpg' }, { label: 'Photo' })
27+
).toBe('https://example.com/a.jpg')
28+
expect(normalizeTelegramMediaParam({ file_id: 'FILE_ID' }, { label: 'Photo' })).toBe('FILE_ID')
29+
expect(normalizeTelegramMediaParam({ fileId: 'FILE_ID_2' }, { label: 'Photo' })).toBe(
30+
'FILE_ID_2'
31+
)
32+
})
33+
34+
it('parses stringified JSON objects/arrays from advanced-mode inputs', () => {
35+
expect(
36+
normalizeTelegramMediaParam('{\"url\":\"https://example.com/a.jpg\"}', { label: 'Photo' })
37+
).toBe('https://example.com/a.jpg')
38+
39+
expect(
40+
normalizeTelegramMediaParam('[{\"url\":\"https://example.com/a.jpg\"}]', { label: 'Photo' })
41+
).toBe('https://example.com/a.jpg')
42+
})
43+
44+
it('rejects missing values with a configurable message', () => {
45+
expect(() =>
46+
normalizeTelegramMediaParam('', { label: 'Photo', errorMessage: 'Photo is required.' })
47+
).toThrow('Photo is required.')
48+
49+
expect(() => normalizeTelegramMediaParam(undefined, { label: 'Photo' })).toThrow(
50+
'Photo URL or file_id is required.'
51+
)
52+
})
53+
54+
it('rejects multiple values when an array is provided', () => {
55+
expect(() =>
56+
normalizeTelegramMediaParam([{ url: 'a' }, { url: 'b' }], { label: 'Photo' })
57+
).toThrow('Photo reference must be a single item, not an array.')
58+
59+
expect(() =>
60+
normalizeTelegramMediaParam('[{\"url\":\"a\"},{\"url\":\"b\"}]', { label: 'Photo' })
61+
).toThrow('Photo reference must be a single item, not an array.')
62+
})
63+
})

apps/sim/tools/telegram/media.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
function isRecord(value: unknown): value is Record<string, unknown> {
2+
return typeof value === 'object' && value !== null && !Array.isArray(value)
3+
}
4+
5+
export function normalizeTelegramMediaParam(
6+
input: unknown,
7+
opts: {
8+
label: string
9+
errorMessage?: string
10+
multipleErrorMessage?: string
11+
}
12+
): string {
13+
const missingMessage = opts.errorMessage ?? `${opts.label} URL or file_id is required.`
14+
const multipleMessage =
15+
opts.multipleErrorMessage ??
16+
`${opts.label} reference must be a single item, not an array. Select one item (e.g. <block.files[0]>).`
17+
18+
if (input === null || input === undefined) {
19+
throw new Error(missingMessage)
20+
}
21+
22+
if (typeof input === 'string') {
23+
const trimmed = input.trim()
24+
if (trimmed.length === 0) {
25+
throw new Error(missingMessage)
26+
}
27+
28+
// Support advanced-mode values that were JSON.stringify'd into short-input fields.
29+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
30+
let parsed: unknown
31+
try {
32+
parsed = JSON.parse(trimmed) as unknown
33+
} catch {
34+
parsed = undefined
35+
}
36+
37+
if (parsed !== undefined) {
38+
return normalizeTelegramMediaParam(parsed, opts)
39+
}
40+
}
41+
42+
return trimmed
43+
}
44+
45+
if (input instanceof URL) {
46+
const asString = input.toString().trim()
47+
if (asString.length > 0) return asString
48+
throw new Error(missingMessage)
49+
}
50+
51+
if (Array.isArray(input)) {
52+
if (input.length === 0) {
53+
throw new Error(missingMessage)
54+
}
55+
if (input.length > 1) {
56+
throw new Error(multipleMessage)
57+
}
58+
return normalizeTelegramMediaParam(input[0], opts)
59+
}
60+
61+
if (isRecord(input)) {
62+
if ('url' in input && typeof input.url === 'string') {
63+
const url = input.url.trim()
64+
if (url.length > 0) return url
65+
}
66+
67+
if ('href' in input && typeof input.href === 'string') {
68+
const href = input.href.trim()
69+
if (href.length > 0) return href
70+
}
71+
72+
if ('file_id' in input && typeof input.file_id === 'string') {
73+
const fileId = input.file_id.trim()
74+
if (fileId.length > 0) return fileId
75+
}
76+
77+
if ('fileId' in input && typeof input.fileId === 'string') {
78+
const fileId = input.fileId.trim()
79+
if (fileId.length > 0) return fileId
80+
}
81+
}
82+
83+
throw new Error(missingMessage)
84+
}

apps/sim/tools/telegram/send_animation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ErrorExtractorId } from '@/tools/error-extractors'
2+
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
23
import type {
34
TelegramMedia,
45
TelegramSendAnimationParams,
@@ -52,9 +53,10 @@ export const telegramSendAnimationTool: ToolConfig<
5253
'Content-Type': 'application/json',
5354
}),
5455
body: (params: TelegramSendAnimationParams) => {
56+
const animation = normalizeTelegramMediaParam(params.animation, { label: 'Animation' })
5557
const body: Record<string, any> = {
5658
chat_id: params.chatId,
57-
animation: params.animation,
59+
animation,
5860
}
5961

6062
if (params.caption) {

apps/sim/tools/telegram/send_audio.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ErrorExtractorId } from '@/tools/error-extractors'
2+
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
23
import type {
34
TelegramAudio,
45
TelegramSendAudioParams,
@@ -50,9 +51,10 @@ export const telegramSendAudioTool: ToolConfig<TelegramSendAudioParams, Telegram
5051
'Content-Type': 'application/json',
5152
}),
5253
body: (params: TelegramSendAudioParams) => {
54+
const audio = normalizeTelegramMediaParam(params.audio, { label: 'Audio' })
5355
const body: Record<string, any> = {
5456
chat_id: params.chatId,
55-
audio: params.audio,
57+
audio,
5658
}
5759

5860
if (params.caption) {

apps/sim/tools/telegram/send_photo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ErrorExtractorId } from '@/tools/error-extractors'
2+
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
23
import type {
34
TelegramPhoto,
45
TelegramSendPhotoParams,
@@ -50,9 +51,10 @@ export const telegramSendPhotoTool: ToolConfig<TelegramSendPhotoParams, Telegram
5051
'Content-Type': 'application/json',
5152
}),
5253
body: (params: TelegramSendPhotoParams) => {
54+
const photo = normalizeTelegramMediaParam(params.photo, { label: 'Photo' })
5355
const body: Record<string, any> = {
5456
chat_id: params.chatId,
55-
photo: params.photo,
57+
photo,
5658
}
5759

5860
if (params.caption) {

apps/sim/tools/telegram/send_video.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ErrorExtractorId } from '@/tools/error-extractors'
2+
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
23
import type {
34
TelegramMedia,
45
TelegramSendMediaResponse,
@@ -50,9 +51,10 @@ export const telegramSendVideoTool: ToolConfig<TelegramSendVideoParams, Telegram
5051
'Content-Type': 'application/json',
5152
}),
5253
body: (params: TelegramSendVideoParams) => {
54+
const video = normalizeTelegramMediaParam(params.video, { label: 'Video' })
5355
const body: Record<string, any> = {
5456
chat_id: params.chatId,
55-
video: params.video,
57+
video,
5658
}
5759

5860
if (params.caption) {

0 commit comments

Comments
 (0)