diff --git a/integrations/email/integration.definition.ts b/integrations/email/integration.definition.ts
index c20e9085c88..ca6ab8c97e2 100644
--- a/integrations/email/integration.definition.ts
+++ b/integrations/email/integration.definition.ts
@@ -11,7 +11,7 @@ const emailSchema = z.object({
export default new IntegrationDefinition({
name: 'email',
- version: '0.0.1',
+ version: '0.1.0',
readme: 'hub.md',
icon: 'icon.svg',
configuration: {
diff --git a/integrations/email/src/imap.ts b/integrations/email/src/imap.ts
index 8f787271fa3..9a9735304e3 100644
--- a/integrations/email/src/imap.ts
+++ b/integrations/email/src/imap.ts
@@ -63,6 +63,8 @@ export const getMessages = async function (
imapBodies.push('TEXT')
}
+ if (box.messages.total === 0) return { messages: [] }
+
const { firstElementIndex, lastElementIndex } = paging.pageToSpan({
page: range.page,
perPage: range.perPage,
diff --git a/integrations/email/src/setup.ts b/integrations/email/src/setup.ts
index 17f48763ea2..f6ee3bc1fe0 100644
--- a/integrations/email/src/setup.ts
+++ b/integrations/email/src/setup.ts
@@ -18,7 +18,10 @@ export const register: bp.IntegrationProps['register'] = async (props) => {
await getMessages({ page: 0, perPage: 1 }, props)
} catch (thrown: unknown) {
const err = thrown instanceof Error ? thrown : new Error(`${thrown}`)
- throw new sdk.RuntimeError('An error occured when registering the integration. Verify your configuration.', err)
+ console.log(err.message)
+ throw new sdk.RuntimeError(
+ `An error occured when registering the integration: ${err.message} Verify your configuration.`
+ )
}
}
diff --git a/integrations/telegram/definitions/channels.ts b/integrations/telegram/definitions/channels.ts
new file mode 100644
index 00000000000..05ee5ac9017
--- /dev/null
+++ b/integrations/telegram/definitions/channels.ts
@@ -0,0 +1,48 @@
+import { z, messages } from '@botpress/sdk'
+
+const _textMessageDefinition = {
+ ...messages.defaults.text,
+ schema: messages.defaults.text.schema.extend({
+ text: messages.defaults.text.schema.shape.text
+ .max(4096)
+ .describe('The text content of the Telegram message (Limit 4096 characters)'),
+ }),
+}
+
+const _imageMessageDefinition = {
+ ...messages.defaults.image,
+ schema: messages.defaults.image.schema.extend({
+ caption: z.string().optional().describe('The caption/description of the image'),
+ }),
+}
+
+const _audioMessageDefinition = {
+ ...messages.defaults.audio,
+ schema: messages.defaults.audio.schema.extend({
+ caption: z.string().optional().describe('The caption/transcription of the audio message'),
+ }),
+}
+
+const _blocSchema = z.union([
+ z.object({ type: z.literal('text'), payload: _textMessageDefinition.schema }),
+ z.object({ type: z.literal('image'), payload: _imageMessageDefinition.schema }),
+ z.object({ type: z.literal('audio'), payload: _audioMessageDefinition.schema }),
+ z.object({ type: z.literal('video'), payload: messages.defaults.video.schema }),
+ z.object({ type: z.literal('file'), payload: messages.defaults.file.schema }),
+ z.object({ type: z.literal('location'), payload: messages.defaults.location.schema }),
+])
+
+const _blocMessageDefinition = {
+ ...messages.defaults.bloc,
+ schema: z.object({
+ items: z.array(_blocSchema),
+ }),
+}
+
+export const telegramMessageChannels = {
+ ...messages.defaults,
+ text: _textMessageDefinition,
+ image: _imageMessageDefinition,
+ audio: _audioMessageDefinition,
+ bloc: _blocMessageDefinition,
+}
diff --git a/integrations/telegram/hub.md b/integrations/telegram/hub.md
index 1652b04a5b3..877c9a346f9 100644
--- a/integrations/telegram/hub.md
+++ b/integrations/telegram/hub.md
@@ -1,3 +1,18 @@
The Telegram integration allows your AI-powered chatbot to seamlessly interact with Telegram, a popular messaging platform with a large user base. Connect your chatbot to Telegram and engage with your audience in real-time conversations. With this integration, you can automate customer support, provide personalized recommendations, send notifications, and handle inquiries directly within Telegram. Leverage Telegram's rich features, including text messages, inline buttons, media files, and more, to create dynamic and interactive chatbot experiences. Empower your chatbot to deliver exceptional user experiences on Telegram with the Telegram Integration for Botpress.
+
+## Migrating from version `0.x.x` to `1.x.x`
+
+### Removal of proactive conversations (and proactive users)
+
+- Telegram does not currently support proactive conversations, so any bots using this feature will need to be updated to use the normal conversation flow.
+
+### Removal of dedicated Markdown messages type
+
+- The `markdown` channel message type is being deprecated in favor of integrating this behavior into the base `text` message type.
+- This new Markdown behavior (commonmark spec) will allow image Markdown. However, since Telegram does not support mixed message types, it will split the message into multiple messages with images sent in between text messages.
+
+### Addition of message limits
+
+- Telegram has a message length limit of 4096 characters, so that limit has been added to the text parameter in the `text` message payload. Going over this limit will result in the message being rejected.
diff --git a/integrations/telegram/integration.definition.ts b/integrations/telegram/integration.definition.ts
index bd50ae907a6..7815abfa300 100644
--- a/integrations/telegram/integration.definition.ts
+++ b/integrations/telegram/integration.definition.ts
@@ -1,11 +1,12 @@
/* bplint-disable */
-import { z, IntegrationDefinition, messages } from '@botpress/sdk'
+import { z, IntegrationDefinition } from '@botpress/sdk'
import { sentry as sentryHelpers } from '@botpress/sdk-addons'
import typingIndicator from './bp_modules/typing-indicator'
+import { telegramMessageChannels } from './definitions/channels'
export default new IntegrationDefinition({
name: 'telegram',
- version: '0.7.4',
+ version: '1.0.0',
title: 'Telegram',
description: 'Engage with your audience in real-time.',
icon: 'icon.svg',
@@ -17,20 +18,10 @@ export default new IntegrationDefinition({
},
channels: {
channel: {
- messages: {
- ...messages.defaults,
- markdown: messages.markdown,
- audio: {
- ...messages.defaults.audio,
- schema: messages.defaults.audio.schema.extend({
- caption: z.string().optional().describe('The caption/transcription of the audio message'),
- }),
- },
- },
+ messages: telegramMessageChannels,
message: { tags: { id: {}, chatId: {} } },
conversation: {
tags: { id: {}, fromUserId: {}, fromUserUsername: {}, fromUserName: {}, chatId: {} },
- creation: { enabled: true, requiredTags: ['id'] },
},
},
},
@@ -41,7 +32,6 @@ export default new IntegrationDefinition({
tags: {
id: {},
},
- creation: { enabled: true, requiredTags: ['id'] },
},
}).extend(typingIndicator, () => ({
entities: {},
diff --git a/integrations/telegram/package.json b/integrations/telegram/package.json
index 5ec290a5796..e55184c5b14 100644
--- a/integrations/telegram/package.json
+++ b/integrations/telegram/package.json
@@ -12,6 +12,9 @@
"@botpress/sdk": "workspace:*",
"@botpress/sdk-addons": "workspace:*",
"lodash": "^4.17.21",
+ "markdown-it": "^14.1.0",
+ "nanoid": "^5.1.5",
+ "sanitize-html": "^2.17.0",
"telegraf": "^4.16.3"
},
"devDependencies": {
@@ -19,7 +22,9 @@
"@botpress/common": "workspace:*",
"@botpresshub/typing-indicator": "workspace:*",
"@sentry/cli": "^2.39.1",
- "@types/lodash": "^4.14.191"
+ "@types/lodash": "^4.14.191",
+ "@types/markdown-it": "^14.1.2",
+ "@types/sanitize-html": "^2.16.0"
},
"bpDependencies": {
"typing-indicator": "../../interfaces/typing-indicator"
diff --git a/integrations/telegram/src/index.ts b/integrations/telegram/src/index.ts
index bdf95d70998..1697d56406c 100644
--- a/integrations/telegram/src/index.ts
+++ b/integrations/telegram/src/index.ts
@@ -1,31 +1,42 @@
-import { RuntimeError } from '@botpress/client'
import { sentry as sentryHelpers } from '@botpress/sdk-addons'
import { ok } from 'assert/strict'
-import { Markup, Telegraf } from 'telegraf'
+import { Telegraf } from 'telegraf'
import type { User } from 'telegraf/typings/core/types/typegram'
+import {
+ handleAudioMessage,
+ handleBlocMessage,
+ handleCardMessage,
+ handleCarouselMessage,
+ handleChoiceMessage,
+ handleDropdownMessage,
+ handleFileMessage,
+ handleImageMessage,
+ handleLocationMessage,
+ handleTextMessage,
+ handleVideoMessage,
+} from './misc/message-handlers'
import { TelegramMessage } from './misc/types'
import {
getUserPictureDataUri,
getUserNameFromTelegramUser,
getChat,
- sendCard,
- ackMessage,
convertTelegramMessageToBotpressMessage,
wrapHandler,
getMessageId,
+ mapToRuntimeErrorAndThrow,
} from './misc/utils'
import * as bp from '.botpress'
const integration = new bp.Integration({
register: async ({ webhookUrl, ctx }) => {
const telegraf = new Telegraf(ctx.configuration.botToken)
- await telegraf.telegram.setWebhook(webhookUrl)
+ await telegraf.telegram.setWebhook(webhookUrl).catch(mapToRuntimeErrorAndThrow)
},
unregister: async ({ ctx }) => {
const telegraf = new Telegraf(ctx.configuration.botToken)
- await telegraf.telegram.deleteWebhook({ drop_pending_updates: true })
+ await telegraf.telegram.deleteWebhook({ drop_pending_updates: true }).catch(mapToRuntimeErrorAndThrow)
},
actions: {
startTypingIndicator: async ({ input, ctx, client }) => {
@@ -36,8 +47,10 @@ const integration = new bp.Integration({
const chat = getChat(conversation)
const messageId = getMessageId(message)
- await telegraf.telegram.sendChatAction(chat, 'typing')
- await telegraf.telegram.setMessageReaction(chat, messageId, [{ type: 'emoji', emoji: 'đ' }])
+ await telegraf.telegram.sendChatAction(chat, 'typing').catch(mapToRuntimeErrorAndThrow)
+ await telegraf.telegram
+ .setMessageReaction(chat, messageId, [{ type: 'emoji', emoji: 'đ' }])
+ .catch(mapToRuntimeErrorAndThrow)
return {}
},
@@ -49,7 +62,7 @@ const integration = new bp.Integration({
const chat = getChat(conversation)
const messageId = getMessageId(message)
- await telegraf.telegram.setMessageReaction(chat, messageId, [])
+ await telegraf.telegram.setMessageReaction(chat, messageId, []).catch(mapToRuntimeErrorAndThrow)
return {}
},
@@ -57,100 +70,17 @@ const integration = new bp.Integration({
channels: {
channel: {
messages: {
- text: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- const { text } = payload
- logger.forBot().debug(`Sending text message to Telegram chat ${chat}:`, text)
- const message = await client.telegram.sendMessage(chat, text)
- await ackMessage(message, ack)
- },
- image: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- logger.forBot().debug(`Sending image message to Telegram chat ${chat}`, payload.imageUrl)
- const message = await client.telegram.sendPhoto(chat, payload.imageUrl)
- await ackMessage(message, ack)
- },
- markdown: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- logger.forBot().debug(`Sending markdown message to Telegram chat ${chat}:`, payload.markdown)
- const message = await client.telegram.sendMessage(chat, payload.markdown, {
- parse_mode: 'MarkdownV2',
- })
- await ackMessage(message, ack)
- },
- audio: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- logger.forBot().debug(`Sending audio voice to Telegram chat ${chat}:`, payload.audioUrl)
- try {
- const message = await client.telegram.sendVoice(chat, payload.audioUrl, { caption: payload.caption })
- await ackMessage(message, ack)
- } catch {
- // If the audio file is too large to be voice, Telegram should send it as an audio file, but if for some reason it doesn't, we can send it as an audio file
- const message = await client.telegram.sendAudio(chat, payload.audioUrl, { caption: payload.caption })
- await ackMessage(message, ack)
- }
- },
- video: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- logger.forBot().debug(`Sending video message to Telegram chat ${chat}:`, payload.videoUrl)
- const message = await client.telegram.sendVideo(chat, payload.videoUrl)
- await ackMessage(message, ack)
- },
- file: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- logger.forBot().debug(`Sending file message to Telegram chat ${chat}:`, payload.fileUrl)
- const message = await client.telegram.sendDocument(chat, payload.fileUrl)
- await ackMessage(message, ack)
- },
- location: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- logger.forBot().debug(`Sending location message to Telegram chat ${chat}:`, {
- latitude: payload.latitude,
- longitude: payload.longitude,
- })
- const message = await client.telegram.sendLocation(chat, payload.latitude, payload.longitude)
- await ackMessage(message, ack)
- },
- card: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- logger.forBot().debug(`Sending card message to Telegram chat ${chat}:`, payload)
- await sendCard(payload, client, chat, ack)
- },
- carousel: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- logger.forBot().debug(`Sending carousel message to Telegram chat ${chat}:`, payload)
- payload.items.forEach(async (item) => {
- await sendCard(item, client, chat, ack)
- })
- },
- dropdown: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- const buttons = payload.options.map((choice) => Markup.button.callback(choice.label, choice.value))
- logger.forBot().debug(`Sending dropdown message to Telegram chat ${chat}:`, payload)
- const message = await client.telegram.sendMessage(chat, payload.text, Markup.keyboard(buttons).oneTime())
- await ackMessage(message, ack)
- },
- choice: async ({ payload, ctx, conversation, ack, logger }) => {
- const client = new Telegraf(ctx.configuration.botToken)
- const chat = getChat(conversation)
- logger.forBot().debug(`Sending choice message to Telegram chat ${chat}:`, payload)
- const buttons = payload.options.map((choice) => Markup.button.callback(choice.label, choice.value))
- const message = await client.telegram.sendMessage(chat, payload.text, Markup.keyboard(buttons).oneTime())
- await ackMessage(message, ack)
- },
- bloc: () => {
- throw new RuntimeError('Not implemented')
- },
+ text: handleTextMessage,
+ image: handleImageMessage,
+ audio: handleAudioMessage,
+ video: handleVideoMessage,
+ file: handleFileMessage,
+ location: handleLocationMessage,
+ card: handleCardMessage,
+ carousel: handleCarouselMessage,
+ dropdown: handleDropdownMessage,
+ choice: handleChoiceMessage,
+ bloc: handleBlocMessage,
},
},
},
@@ -226,6 +156,7 @@ const integration = new bp.Integration({
const bpMessage = await convertTelegramMessageToBotpressMessage({
message,
telegram: telegraf.telegram,
+ logger,
})
logger.forBot().debug(`Received message from user ${telegramUserId}: ${JSON.stringify(message, null, 2)}`)
@@ -240,45 +171,6 @@ const integration = new bp.Integration({
conversationId: conversation.id,
})
}),
- createUser: async ({ client, tags, ctx }) => {
- const strId = tags.id
- const userId = Number(strId)
-
- if (isNaN(userId)) {
- return
- }
-
- const telegraf = new Telegraf(ctx.configuration.botToken)
- const member = await telegraf.telegram.getChatMember(userId, userId)
-
- const { user } = await client.getOrCreateUser({ tags: { id: `${member.user.id}` } })
-
- return {
- body: JSON.stringify({ user: { id: user.id } }),
- headers: {},
- statusCode: 200,
- }
- },
- createConversation: async ({ client, channel, tags, ctx }) => {
- const chatId = tags.id
- if (!chatId) {
- return
- }
-
- const telegraf = new Telegraf(ctx.configuration.botToken)
- const chat = await telegraf.telegram.getChat(chatId)
-
- const { conversation } = await client.getOrCreateConversation({
- channel,
- tags: { id: chat.id.toString() },
- })
-
- return {
- body: JSON.stringify({ conversation: { id: conversation.id } }),
- headers: {},
- statusCode: 200,
- }
- },
})
export default sentryHelpers.wrapIntegration(integration, {
diff --git a/integrations/telegram/src/misc/markdown-to-telegram-html.test.ts b/integrations/telegram/src/misc/markdown-to-telegram-html.test.ts
new file mode 100644
index 00000000000..d27ccdea9ea
--- /dev/null
+++ b/integrations/telegram/src/misc/markdown-to-telegram-html.test.ts
@@ -0,0 +1,435 @@
+import { describe, expect, test } from 'vitest'
+import {
+ markdownHtmlToTelegramPayloads,
+ MarkdownToTelegramHtmlResult,
+ MixedPayloads,
+ stdMarkdownToTelegramHtml,
+} from './markdown-to-telegram-html'
+import { TestCase } from '../../tests/types'
+
+type MarkdownToTelegramHtmlTestCase = TestCase | TestCase
+
+const markdownToTelegramHtmlTestCases: MarkdownToTelegramHtmlTestCase[] = [
+ // ==== Testing each mark type ====
+ {
+ input: '**Bold**',
+ expects: 'Bold',
+ description: 'Apply bold style to text',
+ },
+ {
+ input: '__Bold__',
+ expects: 'Bold',
+ description: 'Alternative apply bold style to text',
+ },
+ {
+ input: '*Italic*',
+ expects: 'Italic',
+ description: 'Apply italic style to text',
+ },
+ {
+ input: '_Italic_',
+ expects: 'Italic',
+ description: 'Alternative apply italic style to text',
+ },
+ {
+ input: '~~Strike~~',
+ expects: 'Strike',
+ description: 'Apply strikethrough style to text',
+ },
+ {
+ input: '||Spoiler||',
+ expects: 'Spoiler',
+ description: 'Apply spoiler style to text',
+ skip: true, // Why? - Feature is not yet implemented
+ },
+ {
+ input: '`Code Snippet`',
+ expects: 'Code Snippet',
+ description: 'Apply code style to text',
+ },
+ {
+ input: '```\nconsole.log("Code Block")\n```',
+ expects: '
console.log("Code Block")\n
',
+ description: 'Apply code block style to text - Without language',
+ },
+ {
+ input: '```typescript\nconsole.log("Code Block")\n```',
+ expects: '
console.log("Code Block")\n
',
+ description: 'Apply code block style to text - With language',
+ },
+ {
+ input: '\tconsole.log("Indented Code Block")',
+ expects: '
console.log("Indented Code Block")\n
',
+ description: 'Apply alternative code block style to text using indentation',
+ },
+ {
+ input: '> Blockquote',
+ expects: '
\n\nBlockquote\n
',
+ description: 'Apply blockquote style to text',
+ },
+ {
+ input: '[Hyperlink](https://www.botpress.com/)',
+ expects: 'Hyperlink',
+ description: 'Convert hyperlink markup to html link',
+ },
+ {
+ input: '[Hyperlink](https://www.botpress.com/ "Tooltip Title")',
+ expects: 'Hyperlink',
+ // NOTE: Telegram does not support the title attribute, however, it just ignores it instead of causing a crash
+ description: 'Markdown hyperlink title gets carried over to html link',
+ },
+ {
+ input: '[Hyperlink][id]\n\n[id]: https://www.botpress.com/ "Tooltip Title"',
+ expects: 'Hyperlink',
+ // NOTE: Telegram does not support the title attribute, however, it just ignores it instead of causing a crash
+ description: 'Convert hyperlink markup using footnote style syntax to html link',
+ },
+ {
+ input: 'https://www.botpress.com/',
+ expects: 'https://www.botpress.com/',
+ description: 'Implicit link gets auto-converted into html link',
+ },
+ {
+ input: '[Phone Number](tel:5141234567)',
+ expects: '5141234567',
+ description:
+ 'Convert phone number markdown to plain text phone number (Telegram does not support "tel" links, but will convert phone numbers into links for us)',
+ },
+ {
+ input: '[Phone Number](tel:5141234567 "Tooltip Title")',
+ expects: '5141234567',
+ description:
+ 'Convert phone number markdown with title attribute to plain text phone number (Telegram does not support "tel" links, but will convert phone numbers into links for us)',
+ },
+ {
+ input: '[Phone Number][id]\n\n[id]: tel:5141234567 "Tooltip Title"',
+ expects: '5141234567',
+ description:
+ 'Convert phone number markdown using footnote style syntax to plain text phone number (Telegram does not support "tel" links, but will convert phone number into links for us)',
+ },
+ {
+ input: '[Botpress Email](mailto:test@botpress.com)',
+ expects: 'test@botpress.com',
+ description:
+ 'Convert email markdown to plain text email address (Telegram does not support "mailto" links, but will convert email addresses into links for us)',
+ },
+ {
+ input: '[Botpress Email](mailto:test@botpress.com "Tooltip Title")',
+ expects: 'test@botpress.com',
+ description:
+ 'Convert email markdown with title attribute to plain text email address (Telegram does not support "mailto" links, but will convert email addresses into links for us)',
+ },
+ {
+ input: '[Botpress Email][id]\n\n[id]: mailto:test@botpress.com "Tooltip Title"',
+ expects: 'test@botpress.com',
+ description:
+ 'Convert email markdown using footnote style syntax to plain text email address (Telegram does not support "mailto" links, but will convert email addresses into links for us)',
+ },
+ {
+ input:
+ '[Botpress Email](mailto:test@botpress.com "Tooltip Title")[Hyperlink](https://www.botpress.com/ "Tooltip Title")',
+ expects: 'test@botpress.comHyperlink',
+ description:
+ "Ensure that the mailto/tel replacer doesn't break normal hyperlinks located immediately after it (Checking for race-condition)",
+ },
+ {
+ input: '',
+ expects: {
+ html: '',
+ extractedData: {
+ images: [
+ {
+ alt: 'Botpress Brand Logo',
+ src: 'https://shop.botpress.com/cdn/shop/files/logo.png?v=1708026010&width=600',
+ pos: 0,
+ },
+ ],
+ },
+ },
+ description: 'Markdown images get extracted since Telegram does not support images embedded into text messages',
+ },
+ {
+ input:
+ '',
+ expects: {
+ html: '',
+ extractedData: {
+ images: [
+ {
+ alt: 'Botpress Brand Logo',
+ src: 'https://shop.botpress.com/cdn/shop/files/logo.png?v=1708026010&width=600',
+ title: 'Title Tooltip Text',
+ pos: 0,
+ },
+ ],
+ },
+ },
+ description: 'Title attribute gets extracted from markdown image',
+ },
+ // ==== Advanced Tests ====
+ {
+ input: '> Blockquote Layer 1\n> > Blockquote Layer 2\n> > > Blockquote Layer 3',
+ expects:
+ '