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
57 changes: 57 additions & 0 deletions apps/sim/blocks/blocks/telegram.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { TelegramBlock } from '@/blocks/blocks/telegram'

describe('TelegramBlock tools.config.params', () => {
it('accepts public photo URLs for telegram_send_photo', () => {
const params = TelegramBlock.tools.config.params({
operation: 'telegram_send_photo',
botToken: 'token',
chatId: ' 123 ',
photo: ' https://example.com/a.jpg ',
caption: 'hello',
} as any)

expect(params).toEqual({
botToken: 'token',
chatId: '123',
photo: 'https://example.com/a.jpg',
caption: 'hello',
})
})

it('accepts stringified JSON photo objects from advanced-mode references', () => {
const params = TelegramBlock.tools.config.params({
operation: 'telegram_send_photo',
botToken: 'token',
chatId: '123',
photo: '{"url":"https://example.com/a.jpg"}',
} as any)

expect(params.photo).toBe('https://example.com/a.jpg')
})

it('supports legacy `withPhoto` alias', () => {
const params = TelegramBlock.tools.config.params({
operation: 'telegram_send_photo',
botToken: 'token',
chatId: '123',
withPhoto: 'https://example.com/a.jpg',
} as any)

expect(params.photo).toBe('https://example.com/a.jpg')
})

it('rejects multiple photo values', () => {
expect(() =>
TelegramBlock.tools.config.params({
operation: 'telegram_send_photo',
botToken: 'token',
chatId: '123',
photo: ['https://example.com/1.jpg', 'https://example.com/2.jpg'],
} as any)
).toThrow('Photo reference must be a single item, not an array.')
})
})
48 changes: 24 additions & 24 deletions apps/sim/blocks/blocks/telegram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TelegramIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
import type { TelegramResponse } from '@/tools/telegram/types'
import { getTrigger } from '@/triggers'

Expand Down Expand Up @@ -269,55 +270,54 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
messageId: params.messageId,
}
case 'telegram_send_photo': {
// photo is the canonical param for both basic (photoFile) and advanced modes
const photoSource = normalizeFileInput(params.photo, {
single: true,
// photo supports both public URLs/file_ids and UserFile objects.
// Backwards-compatible aliases (e.g., `withPhoto`) are supported for older saved workflows.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawPhoto = params.photo ?? (params as any).withPhoto ?? (params as any).with_photo
const photoSource = normalizeTelegramMediaParam(rawPhoto, {
label: 'Photo',
errorMessage: 'Photo is required.',
})
if (!photoSource) {
throw new Error('Photo is required.')
}
return {
...commonParams,
photo: photoSource,
caption: params.caption,
}
}
case 'telegram_send_video': {
// video is the canonical param for both basic (videoFile) and advanced modes
const videoSource = normalizeFileInput(params.video, {
single: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawVideo = params.video ?? (params as any).withVideo ?? (params as any).with_video
const videoSource = normalizeTelegramMediaParam(rawVideo, {
label: 'Video',
errorMessage: 'Video is required.',
})
if (!videoSource) {
throw new Error('Video is required.')
}
return {
...commonParams,
video: videoSource,
caption: params.caption,
}
}
case 'telegram_send_audio': {
// audio is the canonical param for both basic (audioFile) and advanced modes
const audioSource = normalizeFileInput(params.audio, {
single: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawAudio = params.audio ?? (params as any).withAudio ?? (params as any).with_audio
const audioSource = normalizeTelegramMediaParam(rawAudio, {
label: 'Audio',
errorMessage: 'Audio is required.',
})
if (!audioSource) {
throw new Error('Audio is required.')
}
return {
...commonParams,
audio: audioSource,
caption: params.caption,
}
}
case 'telegram_send_animation': {
// animation is the canonical param for both basic (animationFile) and advanced modes
const animationSource = normalizeFileInput(params.animation, {
single: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawAnimation =
params.animation ?? (params as any).withAnimation ?? (params as any).with_animation
const animationSource = normalizeTelegramMediaParam(rawAnimation, {
label: 'Animation',
errorMessage: 'Animation is required.',
})
if (!animationSource) {
throw new Error('Animation is required.')
}
return {
...commonParams,
animation: animationSource,
Expand Down
63 changes: 63 additions & 0 deletions apps/sim/tools/telegram/media.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'

describe('normalizeTelegramMediaParam', () => {
it('accepts trimmed URL/file_id strings', () => {
expect(normalizeTelegramMediaParam(' https://example.com/a.jpg ', { label: 'Photo' })).toBe(
'https://example.com/a.jpg'
)
expect(normalizeTelegramMediaParam(' ABC123 ', { label: 'Photo' })).toBe('ABC123')
})

it('accepts URL instances', () => {
expect(
normalizeTelegramMediaParam(new URL('https://example.com/a.jpg'), { label: 'Photo' })
).toBe('https://example.com/a.jpg')
})

it('accepts object shapes with url/href/file_id', () => {
expect(
normalizeTelegramMediaParam({ url: 'https://example.com/a.jpg' }, { label: 'Photo' })
).toBe('https://example.com/a.jpg')
expect(
normalizeTelegramMediaParam({ href: 'https://example.com/a.jpg' }, { label: 'Photo' })
).toBe('https://example.com/a.jpg')
expect(normalizeTelegramMediaParam({ file_id: 'FILE_ID' }, { label: 'Photo' })).toBe('FILE_ID')
expect(normalizeTelegramMediaParam({ fileId: 'FILE_ID_2' }, { label: 'Photo' })).toBe(
'FILE_ID_2'
)
})

it('parses stringified JSON objects/arrays from advanced-mode inputs', () => {
expect(
normalizeTelegramMediaParam('{\"url\":\"https://example.com/a.jpg\"}', { label: 'Photo' })
).toBe('https://example.com/a.jpg')

expect(
normalizeTelegramMediaParam('[{\"url\":\"https://example.com/a.jpg\"}]', { label: 'Photo' })
).toBe('https://example.com/a.jpg')
})

it('rejects missing values with a configurable message', () => {
expect(() =>
normalizeTelegramMediaParam('', { label: 'Photo', errorMessage: 'Photo is required.' })
).toThrow('Photo is required.')

expect(() => normalizeTelegramMediaParam(undefined, { label: 'Photo' })).toThrow(
'Photo URL or file_id is required.'
)
})

it('rejects multiple values when an array is provided', () => {
expect(() =>
normalizeTelegramMediaParam([{ url: 'a' }, { url: 'b' }], { label: 'Photo' })
).toThrow('Photo reference must be a single item, not an array.')

expect(() =>
normalizeTelegramMediaParam('[{\"url\":\"a\"},{\"url\":\"b\"}]', { label: 'Photo' })
).toThrow('Photo reference must be a single item, not an array.')
})
})
84 changes: 84 additions & 0 deletions apps/sim/tools/telegram/media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

export function normalizeTelegramMediaParam(
input: unknown,
opts: {
label: string
errorMessage?: string
multipleErrorMessage?: string
}
): string {
const missingMessage = opts.errorMessage ?? `${opts.label} URL or file_id is required.`
const multipleMessage =
opts.multipleErrorMessage ??
`${opts.label} reference must be a single item, not an array. Select one item (e.g. <block.files[0]>).`

if (input === null || input === undefined) {
throw new Error(missingMessage)
}

if (typeof input === 'string') {
const trimmed = input.trim()
if (trimmed.length === 0) {
throw new Error(missingMessage)
}

// Support advanced-mode values that were JSON.stringify'd into short-input fields.
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
let parsed: unknown
try {
parsed = JSON.parse(trimmed) as unknown
} catch {
parsed = undefined
}

if (parsed !== undefined) {
return normalizeTelegramMediaParam(parsed, opts)
}
}

return trimmed
}

if (input instanceof URL) {
const asString = input.toString().trim()
if (asString.length > 0) return asString
throw new Error(missingMessage)
}

if (Array.isArray(input)) {
if (input.length === 0) {
throw new Error(missingMessage)
}
if (input.length > 1) {
throw new Error(multipleMessage)
}
return normalizeTelegramMediaParam(input[0], opts)
}

if (isRecord(input)) {
if ('url' in input && typeof input.url === 'string') {
const url = input.url.trim()
if (url.length > 0) return url
}

if ('href' in input && typeof input.href === 'string') {
const href = input.href.trim()
if (href.length > 0) return href
}

if ('file_id' in input && typeof input.file_id === 'string') {
const fileId = input.file_id.trim()
if (fileId.length > 0) return fileId
}

if ('fileId' in input && typeof input.fileId === 'string') {
const fileId = input.fileId.trim()
if (fileId.length > 0) return fileId
}
}

throw new Error(missingMessage)
}
4 changes: 3 additions & 1 deletion apps/sim/tools/telegram/send_animation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
import type {
TelegramMedia,
TelegramSendAnimationParams,
Expand Down Expand Up @@ -52,9 +53,10 @@ export const telegramSendAnimationTool: ToolConfig<
'Content-Type': 'application/json',
}),
body: (params: TelegramSendAnimationParams) => {
const animation = normalizeTelegramMediaParam(params.animation, { label: 'Animation' })
const body: Record<string, any> = {
chat_id: params.chatId,
animation: params.animation,
animation,
}

if (params.caption) {
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/tools/telegram/send_audio.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
import type {
TelegramAudio,
TelegramSendAudioParams,
Expand Down Expand Up @@ -50,9 +51,10 @@ export const telegramSendAudioTool: ToolConfig<TelegramSendAudioParams, Telegram
'Content-Type': 'application/json',
}),
body: (params: TelegramSendAudioParams) => {
const audio = normalizeTelegramMediaParam(params.audio, { label: 'Audio' })
const body: Record<string, any> = {
chat_id: params.chatId,
audio: params.audio,
audio,
}

if (params.caption) {
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/tools/telegram/send_photo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
import type {
TelegramPhoto,
TelegramSendPhotoParams,
Expand Down Expand Up @@ -50,9 +51,10 @@ export const telegramSendPhotoTool: ToolConfig<TelegramSendPhotoParams, Telegram
'Content-Type': 'application/json',
}),
body: (params: TelegramSendPhotoParams) => {
const photo = normalizeTelegramMediaParam(params.photo, { label: 'Photo' })
const body: Record<string, any> = {
chat_id: params.chatId,
photo: params.photo,
photo,
}

if (params.caption) {
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/tools/telegram/send_video.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import { normalizeTelegramMediaParam } from '@/tools/telegram/media'
import type {
TelegramMedia,
TelegramSendMediaResponse,
Expand Down Expand Up @@ -50,9 +51,10 @@ export const telegramSendVideoTool: ToolConfig<TelegramSendVideoParams, Telegram
'Content-Type': 'application/json',
}),
body: (params: TelegramSendVideoParams) => {
const video = normalizeTelegramMediaParam(params.video, { label: 'Video' })
const body: Record<string, any> = {
chat_id: params.chatId,
video: params.video,
video,
}

if (params.caption) {
Expand Down