first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
IHttpRequestMethods,
|
||||
ILoadOptionsFunctions,
|
||||
IRequestOptions,
|
||||
IWebhookFunctions,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { getSendAndWaitConfig } from '../../utils/sendAndWait/utils';
|
||||
import { createUtmCampaignLink } from '../../utils/utilities';
|
||||
|
||||
// Interface in n8n
|
||||
export interface IMarkupKeyboard {
|
||||
rows?: IMarkupKeyboardRow[];
|
||||
}
|
||||
|
||||
export interface IMarkupKeyboardRow {
|
||||
row?: IMarkupKeyboardRow;
|
||||
}
|
||||
|
||||
export interface IMarkupKeyboardRow {
|
||||
buttons?: IMarkupKeyboardButton[];
|
||||
}
|
||||
|
||||
export interface IMarkupKeyboardButton {
|
||||
text: string;
|
||||
additionalFields?: IDataObject;
|
||||
}
|
||||
|
||||
// Interface in Telegram
|
||||
export interface ITelegramInlineReply {
|
||||
inline_keyboard?: ITelegramKeyboardButton[][];
|
||||
}
|
||||
|
||||
export interface ITelegramKeyboardButton {
|
||||
[key: string]: string | number | boolean;
|
||||
}
|
||||
|
||||
export interface ITelegramReplyKeyboard extends IMarkupReplyKeyboardOptions {
|
||||
keyboard: ITelegramKeyboardButton[][];
|
||||
}
|
||||
|
||||
// Shared interfaces
|
||||
export interface IMarkupForceReply {
|
||||
force_reply?: boolean;
|
||||
selective?: boolean;
|
||||
}
|
||||
|
||||
export interface IMarkupReplyKeyboardOptions {
|
||||
one_time_keyboard?: boolean;
|
||||
resize_keyboard?: boolean;
|
||||
selective?: boolean;
|
||||
}
|
||||
|
||||
export interface IMarkupReplyKeyboardRemove {
|
||||
force_reply?: boolean;
|
||||
selective?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the additional fields to the body
|
||||
*
|
||||
* @param {IDataObject} body The body object to add fields to
|
||||
* @param {number} index The index of the item
|
||||
*/
|
||||
export function addAdditionalFields(
|
||||
this: IExecuteFunctions,
|
||||
body: IDataObject,
|
||||
index: number,
|
||||
nodeVersion?: number,
|
||||
instanceId?: string,
|
||||
) {
|
||||
const operation = this.getNodeParameter('operation', index);
|
||||
|
||||
// Add the additional fields
|
||||
const additionalFields = this.getNodeParameter('additionalFields', index);
|
||||
|
||||
if (operation === 'sendMessage') {
|
||||
const attributionText = 'This message was sent automatically with ';
|
||||
const link = createUtmCampaignLink('n8n-nodes-base.telegram', instanceId);
|
||||
|
||||
if (nodeVersion && nodeVersion >= 1.1 && additionalFields.appendAttribution === undefined) {
|
||||
additionalFields.appendAttribution = true;
|
||||
}
|
||||
|
||||
if (!additionalFields.parse_mode) {
|
||||
additionalFields.parse_mode = 'Markdown';
|
||||
}
|
||||
|
||||
const regex = /(https?|ftp|file):\/\/\S+|www\.\S+|\S+\.\S+/;
|
||||
const containsUrl = regex.test(body.text as string);
|
||||
|
||||
if (!containsUrl) {
|
||||
body.disable_web_page_preview = true;
|
||||
}
|
||||
|
||||
if (additionalFields.appendAttribution) {
|
||||
if (additionalFields.parse_mode === 'Markdown') {
|
||||
body.text = `${body.text}\n\n_${attributionText}_[n8n](${link})`;
|
||||
} else if (additionalFields.parse_mode === 'HTML') {
|
||||
body.text = `${body.text}\n\n<em>${attributionText}</em><a href="${link}" target="_blank">n8n</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
nodeVersion &&
|
||||
nodeVersion >= 1.2 &&
|
||||
additionalFields.disable_web_page_preview === undefined
|
||||
) {
|
||||
body.disable_web_page_preview = true;
|
||||
}
|
||||
|
||||
delete additionalFields.appendAttribution;
|
||||
}
|
||||
|
||||
Object.assign(body, additionalFields);
|
||||
|
||||
// Add the reply markup
|
||||
let replyMarkupOption = '';
|
||||
if (operation !== 'sendMediaGroup') {
|
||||
replyMarkupOption = this.getNodeParameter('replyMarkup', index) as string;
|
||||
if (replyMarkupOption === 'none') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
body.reply_markup = {} as
|
||||
| IMarkupForceReply
|
||||
| IMarkupReplyKeyboardRemove
|
||||
| ITelegramInlineReply
|
||||
| ITelegramReplyKeyboard;
|
||||
if (['inlineKeyboard', 'replyKeyboard'].includes(replyMarkupOption)) {
|
||||
let setParameterName = 'inline_keyboard';
|
||||
if (replyMarkupOption === 'replyKeyboard') {
|
||||
setParameterName = 'keyboard';
|
||||
}
|
||||
|
||||
const keyboardData = this.getNodeParameter(replyMarkupOption, index) as IMarkupKeyboard;
|
||||
|
||||
// @ts-ignore
|
||||
(body.reply_markup as ITelegramInlineReply | ITelegramReplyKeyboard)[setParameterName] =
|
||||
[] as ITelegramKeyboardButton[][];
|
||||
let sendButtonData: ITelegramKeyboardButton;
|
||||
if (keyboardData.rows !== undefined) {
|
||||
for (const row of keyboardData.rows) {
|
||||
const sendRows: ITelegramKeyboardButton[] = [];
|
||||
if (row.row?.buttons === undefined) {
|
||||
continue;
|
||||
}
|
||||
for (const button of row.row.buttons) {
|
||||
sendButtonData = {};
|
||||
sendButtonData.text = button.text;
|
||||
if (button.additionalFields) {
|
||||
Object.assign(sendButtonData, button.additionalFields);
|
||||
}
|
||||
sendRows.push(sendButtonData);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const array = (body.reply_markup as ITelegramInlineReply | ITelegramReplyKeyboard)[
|
||||
setParameterName
|
||||
] as ITelegramKeyboardButton[][];
|
||||
array.push(sendRows);
|
||||
}
|
||||
}
|
||||
} else if (replyMarkupOption === 'forceReply') {
|
||||
const forceReply = this.getNodeParameter('forceReply', index) as IMarkupForceReply;
|
||||
body.reply_markup = forceReply;
|
||||
} else if (replyMarkupOption === 'replyKeyboardRemove') {
|
||||
const forceReply = this.getNodeParameter(
|
||||
'replyKeyboardRemove',
|
||||
index,
|
||||
) as IMarkupReplyKeyboardRemove;
|
||||
body.reply_markup = forceReply;
|
||||
}
|
||||
|
||||
if (replyMarkupOption === 'replyKeyboard') {
|
||||
const replyKeyboardOptions = this.getNodeParameter(
|
||||
'replyKeyboardOptions',
|
||||
index,
|
||||
) as IMarkupReplyKeyboardOptions;
|
||||
Object.assign(body.reply_markup, replyKeyboardOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an API request to Telegram
|
||||
*
|
||||
*/
|
||||
export async function apiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
body: IDataObject,
|
||||
query?: IDataObject,
|
||||
option: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const credentials = await this.getCredentials('telegramApi');
|
||||
|
||||
query = query || {};
|
||||
|
||||
const options: IRequestOptions = {
|
||||
headers: {},
|
||||
method,
|
||||
uri: `${credentials.baseUrl}/bot${credentials.accessToken}/${endpoint}`,
|
||||
body,
|
||||
qs: query,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (Object.keys(option).length > 0) {
|
||||
Object.assign(options, option);
|
||||
}
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
if (Object.keys(query).length === 0) {
|
||||
delete options.qs;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.helpers.request(options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
export function getImageBySize(photos: IDataObject[], size: string): IDataObject | undefined {
|
||||
const sizes = {
|
||||
small: 0,
|
||||
medium: 1,
|
||||
large: 2,
|
||||
extraLarge: 3,
|
||||
} as IDataObject;
|
||||
|
||||
const index = sizes[size] as number;
|
||||
|
||||
return photos[index];
|
||||
}
|
||||
|
||||
export function getPropertyName(operation: string) {
|
||||
return operation.replace('send', '').toLowerCase();
|
||||
}
|
||||
|
||||
export function getSecretToken(this: IHookFunctions | IWebhookFunctions) {
|
||||
// Only characters A-Z, a-z, 0-9, _ and - are allowed.
|
||||
const secret_token = `${this.getWorkflow().id}_${this.getNode().id}`;
|
||||
return secret_token.replace(/[^a-zA-Z0-9\_\-]+/g, '');
|
||||
}
|
||||
|
||||
export function createSendAndWaitMessageBody(context: IExecuteFunctions) {
|
||||
const chat_id = context.getNodeParameter('chatId', 0) as string;
|
||||
|
||||
const config = getSendAndWaitConfig(context);
|
||||
let text = config.message;
|
||||
|
||||
if (config.appendAttribution !== false) {
|
||||
const instanceId = context.getInstanceId();
|
||||
const attributionText = 'This message was sent automatically with ';
|
||||
const link = createUtmCampaignLink('n8n-nodes-base.telegram', instanceId);
|
||||
text = `${text}\n\n_${attributionText}_[n8n](${link})`;
|
||||
}
|
||||
|
||||
const body = {
|
||||
chat_id,
|
||||
text,
|
||||
|
||||
disable_web_page_preview: true,
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
config.options.map((option) => {
|
||||
return {
|
||||
text: option.label,
|
||||
url: option.url,
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return body;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
interface EventBody {
|
||||
photo?: [
|
||||
{
|
||||
file_id: string;
|
||||
},
|
||||
];
|
||||
document?: {
|
||||
file_id: string;
|
||||
};
|
||||
video?: {
|
||||
file_id: string;
|
||||
};
|
||||
chat?: {
|
||||
id: number;
|
||||
};
|
||||
from?: {
|
||||
id: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IEvent {
|
||||
message?: EventBody;
|
||||
channel_post?: EventBody;
|
||||
download_link?: string;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.telegram",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication", "HITL"],
|
||||
"subcategories": {
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"alias": ["human", "form", "wait", "hitl", "approval"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/telegram/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.telegram/"
|
||||
}
|
||||
],
|
||||
"generic": [
|
||||
{
|
||||
"label": "Why business process automation with n8n can change your daily life",
|
||||
"icon": "🧬",
|
||||
"url": "https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/"
|
||||
},
|
||||
{
|
||||
"label": "Create a toxic language detector for Telegram in 4 step",
|
||||
"icon": "🤬",
|
||||
"url": "https://n8n.io/blog/create-a-toxic-language-detector-for-telegram/"
|
||||
},
|
||||
{
|
||||
"label": "Automatically Adding Expense Receipts to Google Sheets with Telegram, Mindee, Twilio, and n8n",
|
||||
"icon": "🧾",
|
||||
"url": "https://n8n.io/blog/automatically-adding-expense-receipts-to-google-sheets-with-telegram-mindee-twilio-and-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "6 e-commerce workflows to power up your Shopify s",
|
||||
"icon": "store",
|
||||
"url": "https://n8n.io/blog/no-code-ecommerce-workflow-automations/"
|
||||
},
|
||||
{
|
||||
"label": "Celebrating World Poetry Day with a daily poem in Telegram",
|
||||
"icon": "📜",
|
||||
"url": "https://n8n.io/blog/world-poetry-day-workflow/"
|
||||
},
|
||||
{
|
||||
"label": "Using Automation to Boost Productivity in the Workplace",
|
||||
"icon": "💪",
|
||||
"url": "https://n8n.io/blog/using-automation-to-boost-productivity-in-the-workplace/"
|
||||
},
|
||||
{
|
||||
"label": "How to set up a no-code CI/CD pipeline with GitHub and TravisCI",
|
||||
"icon": "🎡",
|
||||
"url": "https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/"
|
||||
},
|
||||
{
|
||||
"label": "Creating scheduled text affirmations with n8n",
|
||||
"icon": "🤟",
|
||||
"url": "https://n8n.io/blog/creating-scheduled-text-affirmations-with-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "Creating Telegram Bots with n8n, a No-Code Platform",
|
||||
"icon": "💬",
|
||||
"url": "https://n8n.io/blog/creating-telegram-bots-with-n8n-a-no-code-platform/"
|
||||
},
|
||||
{
|
||||
"label": "7 no-code workflow automations for Amazon Web Services",
|
||||
"url": "https://n8n.io/blog/aws-workflow-automation/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.telegramTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/telegram/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.telegramtrigger/"
|
||||
}
|
||||
],
|
||||
"generic": [
|
||||
{
|
||||
"label": "Create a toxic language detector for Telegram in 4 step",
|
||||
"icon": "🤬",
|
||||
"url": "https://n8n.io/blog/create-a-toxic-language-detector-for-telegram/"
|
||||
},
|
||||
{
|
||||
"label": "Automatically Adding Expense Receipts to Google Sheets with Telegram, Mindee, Twilio, and n8n",
|
||||
"icon": "🧾",
|
||||
"url": "https://n8n.io/blog/automatically-adding-expense-receipts-to-google-sheets-with-telegram-mindee-twilio-and-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "How to set up a no-code CI/CD pipeline with GitHub and TravisCI",
|
||||
"icon": "🎡",
|
||||
"url": "https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/"
|
||||
},
|
||||
{
|
||||
"label": "Creating Telegram Bots with n8n, a No-Code Platform",
|
||||
"icon": "💬",
|
||||
"url": "https://n8n.io/blog/creating-telegram-bots-with-n8n-a-no-code-platform/"
|
||||
},
|
||||
{
|
||||
"label": "7 no-code workflow automations for Amazon Web Services",
|
||||
"url": "https://n8n.io/blog/aws-workflow-automation/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import crypto from 'crypto';
|
||||
import type {
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
IDataObject,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IWebhookResponseData,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest, getSecretToken } from './GenericFunctions';
|
||||
import type { IEvent } from './IEvent';
|
||||
import { downloadFile } from './util/triggerUtils';
|
||||
|
||||
export class TelegramTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Telegram Trigger',
|
||||
name: 'telegramTrigger',
|
||||
icon: 'file:telegram.svg',
|
||||
group: ['trigger'],
|
||||
version: [1, 1.1, 1.2],
|
||||
defaultVersion: 1.2,
|
||||
subtitle: '=Updates: {{$parameter["updates"].join(", ")}}',
|
||||
description: 'Starts the workflow on a Telegram update',
|
||||
defaults: {
|
||||
name: 'Telegram Trigger',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'telegramApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName:
|
||||
'Due to Telegram API limitations, you can use just one Telegram trigger for each bot at a time',
|
||||
name: 'telegramTriggerNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Trigger On',
|
||||
name: 'updates',
|
||||
type: 'multiOptions',
|
||||
options: [
|
||||
{
|
||||
name: '*',
|
||||
value: '*',
|
||||
description: 'All updates',
|
||||
},
|
||||
{
|
||||
name: 'Callback Query',
|
||||
value: 'callback_query',
|
||||
description: 'Trigger on new incoming callback query',
|
||||
},
|
||||
{
|
||||
name: 'Channel Post',
|
||||
value: 'channel_post',
|
||||
description:
|
||||
'Trigger on new incoming channel post of any kind — text, photo, sticker, etc',
|
||||
},
|
||||
{
|
||||
name: 'Edited Channel Post',
|
||||
value: 'edited_channel_post',
|
||||
description:
|
||||
'Trigger on new version of a channel post that is known to the bot and was edited',
|
||||
},
|
||||
{
|
||||
name: 'Edited Message',
|
||||
value: 'edited_message',
|
||||
description:
|
||||
'Trigger on new version of a channel post that is known to the bot and was edited',
|
||||
},
|
||||
{
|
||||
name: 'Inline Query',
|
||||
value: 'inline_query',
|
||||
description: 'Trigger on new incoming inline query',
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
value: 'message',
|
||||
description: 'Trigger on new incoming message of any kind — text, photo, sticker, etc',
|
||||
},
|
||||
{
|
||||
name: 'Poll',
|
||||
value: 'poll',
|
||||
action: 'On Poll Change',
|
||||
description:
|
||||
'Trigger on new poll state. Bots receive only updates about stopped polls and polls, which are sent by the bot.',
|
||||
},
|
||||
{
|
||||
name: 'Pre-Checkout Query',
|
||||
value: 'pre_checkout_query',
|
||||
description:
|
||||
'Trigger on new incoming pre-checkout query. Contains full information about checkout.',
|
||||
},
|
||||
{
|
||||
name: 'Shipping Query',
|
||||
value: 'shipping_query',
|
||||
description:
|
||||
'Trigger on new incoming shipping query. Only for invoices with flexible price.',
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
default: [],
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Every uploaded attachment, even if sent in a group, will trigger a separate event. You can identify that an attachment belongs to a certain group by <code>media_group_id</code> .',
|
||||
name: 'attachmentNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Download Images/Files',
|
||||
name: 'download',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description:
|
||||
"Telegram delivers the image in multiple sizes. By default, just the large image would be downloaded. If you want to change the size, set the field 'Image Size'.",
|
||||
},
|
||||
{
|
||||
displayName: 'Image Size',
|
||||
name: 'imageSize',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
download: [true],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Small',
|
||||
value: 'small',
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
value: 'medium',
|
||||
},
|
||||
{
|
||||
name: 'Large',
|
||||
value: 'large',
|
||||
},
|
||||
{
|
||||
name: 'Extra Large',
|
||||
value: 'extraLarge',
|
||||
},
|
||||
],
|
||||
default: 'large',
|
||||
description: 'The size of the image to be downloaded',
|
||||
},
|
||||
{
|
||||
displayName: 'Restrict to Chat IDs',
|
||||
name: 'chatIds',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'The chat IDs to restrict the trigger to. Multiple can be defined separated by comma.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.1 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Restrict to User IDs',
|
||||
name: 'userIds',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'The user IDs to restrict the trigger to. Multiple can be defined separated by comma.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.1 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const endpoint = 'getWebhookInfo';
|
||||
const webhookReturnData = await apiRequest.call(this, 'POST', endpoint, {});
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
|
||||
if (webhookReturnData.result.url === webhookUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
|
||||
let allowedUpdates = this.getNodeParameter('updates') as string[];
|
||||
|
||||
if ((allowedUpdates || []).includes('*')) {
|
||||
allowedUpdates = [];
|
||||
}
|
||||
|
||||
const endpoint = 'setWebhook';
|
||||
|
||||
const secret_token = getSecretToken.call(this);
|
||||
|
||||
const body = {
|
||||
url: webhookUrl,
|
||||
allowed_updates: allowedUpdates,
|
||||
secret_token,
|
||||
};
|
||||
|
||||
await apiRequest.call(this, 'POST', endpoint, body);
|
||||
|
||||
return true;
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
const endpoint = 'deleteWebhook';
|
||||
const body = {};
|
||||
|
||||
try {
|
||||
await apiRequest.call(this, 'POST', endpoint, body);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const credentials = await this.getCredentials('telegramApi');
|
||||
|
||||
const bodyData = this.getBodyData() as IEvent;
|
||||
const headerData = this.getHeaderData();
|
||||
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
if (nodeVersion > 1) {
|
||||
const secret = getSecretToken.call(this);
|
||||
const secretBuffer = Buffer.from(secret);
|
||||
const headerSecretBuffer = Buffer.from(
|
||||
String(headerData['x-telegram-bot-api-secret-token'] ?? ''),
|
||||
);
|
||||
if (
|
||||
secretBuffer.byteLength !== headerSecretBuffer.byteLength ||
|
||||
!crypto.timingSafeEqual(secretBuffer, headerSecretBuffer)
|
||||
) {
|
||||
const res = this.getResponseObject();
|
||||
res.status(403).json({ message: 'Provided secret is not valid' });
|
||||
return {
|
||||
noWebhookResponse: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields') as IDataObject;
|
||||
|
||||
if (additionalFields.download) {
|
||||
const downloadFilesResult = await downloadFile(this, credentials, bodyData, additionalFields);
|
||||
|
||||
if (Object.entries(downloadFilesResult).length !== 0) return downloadFilesResult;
|
||||
}
|
||||
|
||||
if (nodeVersion >= 1.2) {
|
||||
if (additionalFields.chatIds) {
|
||||
const chatIds = additionalFields.chatIds as string;
|
||||
const splitIds = chatIds.split(',').map((chatId) => chatId.trim());
|
||||
if (!splitIds.includes(String(bodyData.message?.chat?.id))) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalFields.userIds) {
|
||||
const userIds = additionalFields.userIds as string;
|
||||
const splitIds = userIds.split(',').map((userId) => userId.trim());
|
||||
if (!splitIds.includes(String(bodyData.message?.from?.id))) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
workflowData: [this.helpers.returnJsonArray([bodyData as unknown as IDataObject])],
|
||||
};
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"can_be_edited": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_change_info": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_delete_messages": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_delete_stories": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_edit_stories": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_invite_users": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_manage_chat": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_manage_topics": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_manage_video_chats": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_manage_voice_chats": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_pin_messages": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_post_stories": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_promote_members": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_restrict_members": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_anonymous": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"language_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accent_color_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"accepted_gift_types": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limited_gifts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"premium_subscription": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"unique_gifts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"unlimited_gifts": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"active_usernames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"can_send_gift": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_reaction_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"is_anonymous": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"language_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 6
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"edit_date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"mime_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumb": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"document": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mime_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumb": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"audio": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mime_type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"document": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mime_type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"latitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"caption": {
|
||||
"type": "string"
|
||||
},
|
||||
"chat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"photo": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"entities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"length": {
|
||||
"type": "integer"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"link_preview_options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"is_disabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"caption": {
|
||||
"type": "string"
|
||||
},
|
||||
"chat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"photo": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"sticker": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"emoji": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_animated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_video": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"set_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumb": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"caption": {
|
||||
"type": "string"
|
||||
},
|
||||
"chat": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"type": "integer"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_bot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"video": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"mime_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumb": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"file_unique_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 66 66"><use xlink:href="#a" x=".5" y=".5"/><symbol id="a" overflow="visible"><g fill-rule="nonzero" stroke="none"><path fill="#37aee2" d="M0 32c0 17.673 14.327 32 32 32s32-14.327 32-32S49.673 0 32 0 0 14.327 0 32"/><path fill="#c8daea" d="m21.661 34.338 3.797 10.508s.475.983.983.983 8.068-7.864 8.068-7.864l8.407-16.237-21.119 9.898z"/><path fill="#a9c6d8" d="m26.695 37.034-.729 7.746s-.305 2.373 2.068 0l4.644-4.203"/><path d="m21.73 34.712-7.809-2.545s-.932-.378-.633-1.237c.062-.177.186-.328.559-.588 1.731-1.206 32.028-12.096 32.028-12.096s.856-.288 1.361-.097c.231.088.378.187.503.548.045.132.071.411.068.689-.003.201-.027.386-.045.678-.184 2.978-5.706 25.198-5.706 25.198s-.33 1.3-1.514 1.345c-.432.016-.956-.071-1.582-.61-2.323-1.998-10.352-7.394-12.126-8.58a.34.34 0 0 1-.146-.239c-.025-.125.108-.28.108-.28s13.98-12.427 14.352-13.731c.029-.101-.079-.151-.226-.107-.929.342-17.025 10.506-18.801 11.629-.104.066-.395.023-.395.023"/></g></symbol></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,335 @@
|
||||
import {
|
||||
NodeApiError,
|
||||
type IDataObject,
|
||||
type IExecuteFunctions,
|
||||
type IHookFunctions,
|
||||
type IHttpRequestMethods,
|
||||
type ILoadOptionsFunctions,
|
||||
type IWebhookFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
addAdditionalFields,
|
||||
apiRequest,
|
||||
getPropertyName,
|
||||
getSecretToken,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
describe('Telegram > GenericFunctions', () => {
|
||||
describe('apiRequest', () => {
|
||||
let mockThis: IHookFunctions & IExecuteFunctions & ILoadOptionsFunctions & IWebhookFunctions;
|
||||
const credentials = { baseUrl: 'https://api.telegram.org', accessToken: 'testToken' };
|
||||
beforeEach(() => {
|
||||
mockThis = {
|
||||
getCredentials: jest.fn(),
|
||||
helpers: {
|
||||
request: jest.fn(),
|
||||
},
|
||||
getNode: jest.fn(),
|
||||
} as unknown as IHookFunctions &
|
||||
IExecuteFunctions &
|
||||
ILoadOptionsFunctions &
|
||||
IWebhookFunctions;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should make a successful API request', async () => {
|
||||
const method: IHttpRequestMethods = 'POST';
|
||||
const endpoint = 'sendMessage';
|
||||
const body: IDataObject = { text: 'Hello, world!' };
|
||||
const query: IDataObject = { chat_id: '12345' };
|
||||
const option: IDataObject = { headers: { 'Custom-Header': 'value' } };
|
||||
|
||||
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
|
||||
(mockThis.helpers.request as jest.Mock).mockResolvedValue({ success: true });
|
||||
|
||||
const result = await apiRequest.call(mockThis, method, endpoint, body, query, option);
|
||||
|
||||
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
|
||||
expect(mockThis.helpers.request).toHaveBeenCalledWith({
|
||||
headers: { 'Custom-Header': 'value' },
|
||||
method: 'POST',
|
||||
uri: 'https://api.telegram.org/bottestToken/sendMessage',
|
||||
body: { text: 'Hello, world!' },
|
||||
qs: { chat_id: '12345' },
|
||||
json: true,
|
||||
});
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should handle an API request with no body and query', async () => {
|
||||
const method: IHttpRequestMethods = 'GET';
|
||||
const endpoint = 'getMe';
|
||||
const body: IDataObject = {};
|
||||
const query: IDataObject = {};
|
||||
|
||||
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
|
||||
(mockThis.helpers.request as jest.Mock).mockResolvedValue({ success: true });
|
||||
|
||||
const result = await apiRequest.call(mockThis, method, endpoint, body, query);
|
||||
|
||||
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
|
||||
expect(mockThis.helpers.request).toHaveBeenCalledWith({
|
||||
headers: {},
|
||||
method: 'GET',
|
||||
uri: 'https://api.telegram.org/bottestToken/getMe',
|
||||
json: true,
|
||||
});
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should handle an API request with no additional options', async () => {
|
||||
const method: IHttpRequestMethods = 'POST';
|
||||
const endpoint = 'sendMessage';
|
||||
const body: IDataObject = { text: 'Hello, world!' };
|
||||
|
||||
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
|
||||
(mockThis.helpers.request as jest.Mock).mockResolvedValue({ success: true });
|
||||
|
||||
const result = await apiRequest.call(mockThis, method, endpoint, body);
|
||||
|
||||
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
|
||||
expect(mockThis.helpers.request).toHaveBeenCalledWith({
|
||||
headers: {},
|
||||
method: 'POST',
|
||||
uri: 'https://api.telegram.org/bottestToken/sendMessage',
|
||||
body: { text: 'Hello, world!' },
|
||||
json: true,
|
||||
});
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should throw a NodeApiError on request failure', async () => {
|
||||
const method: IHttpRequestMethods = 'POST';
|
||||
const endpoint = 'sendMessage';
|
||||
const body: IDataObject = { text: 'Hello, world!' };
|
||||
|
||||
(mockThis.getCredentials as jest.Mock).mockResolvedValue(credentials);
|
||||
(mockThis.helpers.request as jest.Mock).mockRejectedValue(new Error('Request failed'));
|
||||
|
||||
await expect(apiRequest.call(mockThis, method, endpoint, body)).rejects.toThrow(NodeApiError);
|
||||
|
||||
expect(mockThis.getCredentials).toHaveBeenCalledWith('telegramApi');
|
||||
expect(mockThis.helpers.request).toHaveBeenCalledWith({
|
||||
headers: {},
|
||||
method: 'POST',
|
||||
uri: 'https://api.telegram.org/bottestToken/sendMessage',
|
||||
body: { text: 'Hello, world!' },
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('addAdditionalFields', () => {
|
||||
let mockThis: IExecuteFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThis = {
|
||||
getNodeParameter: jest.fn(),
|
||||
} as unknown as IExecuteFunctions;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should add additional fields and attribution for sendMessage operation', () => {
|
||||
const body: IDataObject = { text: 'Hello, world!' };
|
||||
const index = 0;
|
||||
const nodeVersion = 1.1;
|
||||
const instanceId = '45';
|
||||
|
||||
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
switch (paramName) {
|
||||
case 'operation':
|
||||
return 'sendMessage';
|
||||
case 'additionalFields':
|
||||
return { appendAttribution: true };
|
||||
case 'replyMarkup':
|
||||
return 'none';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
addAdditionalFields.call(mockThis, body, index, nodeVersion, instanceId);
|
||||
|
||||
expect(body).toEqual({
|
||||
text: 'Hello, world!\n\n_This message was sent automatically with _[n8n](https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_45)',
|
||||
parse_mode: 'Markdown',
|
||||
disable_web_page_preview: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should add reply markup for inlineKeyboard', () => {
|
||||
const body: IDataObject = { text: 'Hello, world!' };
|
||||
const index = 0;
|
||||
|
||||
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
switch (paramName) {
|
||||
case 'operation':
|
||||
return 'sendMessage';
|
||||
case 'additionalFields':
|
||||
return {};
|
||||
case 'replyMarkup':
|
||||
return 'inlineKeyboard';
|
||||
case 'inlineKeyboard':
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
row: {
|
||||
buttons: [
|
||||
{ text: 'Button 1', additionalFields: { url: 'https://example.com' } },
|
||||
{ text: 'Button 2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
addAdditionalFields.call(mockThis, body, index);
|
||||
|
||||
expect(body).toEqual({
|
||||
text: 'Hello, world!',
|
||||
disable_web_page_preview: true,
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[{ text: 'Button 1', url: 'https://example.com' }, { text: 'Button 2' }],
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should add reply markup for forceReply', () => {
|
||||
const body: IDataObject = { text: 'Hello, world!' };
|
||||
const index = 0;
|
||||
|
||||
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
switch (paramName) {
|
||||
case 'operation':
|
||||
return 'sendMessage';
|
||||
case 'additionalFields':
|
||||
return {};
|
||||
case 'replyMarkup':
|
||||
return 'forceReply';
|
||||
case 'forceReply':
|
||||
return { force_reply: true };
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
addAdditionalFields.call(mockThis, body, index);
|
||||
|
||||
expect(body).toEqual({
|
||||
text: 'Hello, world!',
|
||||
disable_web_page_preview: true,
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: { force_reply: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should add reply markup for replyKeyboardRemove', () => {
|
||||
const body: IDataObject = { text: 'Hello, world!' };
|
||||
const index = 0;
|
||||
|
||||
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
switch (paramName) {
|
||||
case 'operation':
|
||||
return 'sendMessage';
|
||||
case 'additionalFields':
|
||||
return {};
|
||||
case 'replyMarkup':
|
||||
return 'replyKeyboardRemove';
|
||||
case 'replyKeyboardRemove':
|
||||
return { remove_keyboard: true };
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
addAdditionalFields.call(mockThis, body, index);
|
||||
|
||||
expect(body).toEqual({
|
||||
text: 'Hello, world!',
|
||||
disable_web_page_preview: true,
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: { remove_keyboard: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nodeVersion 1.2 and set disable_web_page_preview', () => {
|
||||
const body: IDataObject = { text: 'Hello, world!' };
|
||||
const index = 0;
|
||||
const nodeVersion = 1.2;
|
||||
|
||||
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
switch (paramName) {
|
||||
case 'operation':
|
||||
return 'sendMessage';
|
||||
case 'additionalFields':
|
||||
return {};
|
||||
case 'replyMarkup':
|
||||
return 'none';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
addAdditionalFields.call(mockThis, body, index, nodeVersion);
|
||||
|
||||
expect(body).toEqual({
|
||||
disable_web_page_preview: true,
|
||||
parse_mode: 'Markdown',
|
||||
text: 'Hello, world!\n\n_This message was sent automatically with _[n8n](https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram)',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('getPropertyName', () => {
|
||||
it('should return the property name by removing "send" and converting to lowercase', () => {
|
||||
expect(getPropertyName('sendMessage')).toBe('message');
|
||||
expect(getPropertyName('sendEmail')).toBe('email');
|
||||
expect(getPropertyName('sendNotification')).toBe('notification');
|
||||
});
|
||||
|
||||
it('should return the original string in lowercase if it does not contain "send"', () => {
|
||||
expect(getPropertyName('receiveMessage')).toBe('receivemessage');
|
||||
expect(getPropertyName('fetchData')).toBe('fetchdata');
|
||||
});
|
||||
|
||||
it('should return an empty string if the input is "send"', () => {
|
||||
expect(getPropertyName('send')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
expect(getPropertyName('')).toBe('');
|
||||
});
|
||||
});
|
||||
describe('getSecretToken', () => {
|
||||
const mockThis = {
|
||||
getWorkflow: jest.fn().mockReturnValue({ id: 'workflow123' }),
|
||||
getNode: jest.fn().mockReturnValue({ id: 'node123' }),
|
||||
} as unknown as IHookFunctions & IWebhookFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return a valid secret token', () => {
|
||||
const secretToken = getSecretToken.call(mockThis);
|
||||
|
||||
expect(secretToken).toBe('workflow123_node123');
|
||||
});
|
||||
|
||||
it('should remove invalid characters from the secret token', () => {
|
||||
mockThis.getNode().id = 'node@123';
|
||||
mockThis.getWorkflow().id = 'workflow#123';
|
||||
|
||||
const secretToken = getSecretToken.call(mockThis);
|
||||
expect(secretToken).toBe('workflow123_node123');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import get from 'lodash/get';
|
||||
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
|
||||
|
||||
export const telegramNode: INode = {
|
||||
id: 'b3039263-29ad-4476-9894-51dfcc5a706d',
|
||||
name: 'Telegram node',
|
||||
typeVersion: 1.2,
|
||||
type: 'n8n-nodes-base.telegram',
|
||||
position: [0, 0],
|
||||
parameters: {
|
||||
resource: 'callback',
|
||||
operation: 'answerQuery',
|
||||
},
|
||||
};
|
||||
|
||||
export const createMockExecuteFunction = (nodeParameters: IDataObject) => {
|
||||
const fakeExecuteFunction = {
|
||||
getInputData() {
|
||||
return [{ json: {} }];
|
||||
},
|
||||
getNodeParameter(
|
||||
parameterName: string,
|
||||
_itemIndex: number,
|
||||
fallbackValue?: IDataObject,
|
||||
options?: IGetNodeParameterOptions,
|
||||
) {
|
||||
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
|
||||
return get(nodeParameters, parameter, fallbackValue);
|
||||
},
|
||||
getNode() {
|
||||
return telegramNode;
|
||||
},
|
||||
helpers: {},
|
||||
continueOnFail: () => false,
|
||||
} as unknown as IExecuteFunctions;
|
||||
return fakeExecuteFunction;
|
||||
};
|
||||
@@ -0,0 +1,753 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
NodeExecutionWithMetadata,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import * as GenericFunctions from '../GenericFunctions';
|
||||
import { Telegram } from '../Telegram.node';
|
||||
|
||||
describe('Telegram node', () => {
|
||||
const executeFunctionsMock = mockDeep<IExecuteFunctions>();
|
||||
const apiRequestSpy = jest.spyOn(GenericFunctions, 'apiRequest');
|
||||
const node = new Telegram();
|
||||
|
||||
const legacyBinaryAccessHelper = (index: number, propertyName: string | any) => {
|
||||
const items = executeFunctionsMock.getInputData();
|
||||
return items[index].binary![propertyName as string];
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
executeFunctionsMock.getCredentials.mockResolvedValue({
|
||||
baseUrl: 'https://api.telegram.org',
|
||||
accessToken: 'test-token',
|
||||
});
|
||||
executeFunctionsMock.getNode.mockReturnValue({
|
||||
typeVersion: 1.2,
|
||||
} as INode);
|
||||
executeFunctionsMock.getInputData.mockReturnValue([{ json: {} }]);
|
||||
executeFunctionsMock.helpers.returnJsonArray.mockImplementation(
|
||||
(input) => input as INodeExecutionData[],
|
||||
);
|
||||
executeFunctionsMock.helpers.constructExecutionMetaData.mockImplementation(
|
||||
(input) => input as NodeExecutionWithMetadata[],
|
||||
);
|
||||
});
|
||||
|
||||
describe('file:get', () => {
|
||||
beforeEach(() => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((p) => {
|
||||
switch (p) {
|
||||
case 'resource':
|
||||
return 'file';
|
||||
case 'operation':
|
||||
return 'get';
|
||||
case 'download':
|
||||
return true;
|
||||
case 'fileId':
|
||||
return 'file-id';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should determine the mime type of the file', async () => {
|
||||
apiRequestSpy.mockResolvedValueOnce({
|
||||
result: {
|
||||
file_id: 'file-id',
|
||||
file_path: 'documents/file_1.pdf',
|
||||
},
|
||||
});
|
||||
apiRequestSpy.mockResolvedValueOnce({
|
||||
body: Buffer.from('test-file'),
|
||||
});
|
||||
executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
|
||||
data: 'test-file',
|
||||
mimeType: 'application/pdf',
|
||||
});
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
result: {
|
||||
file_id: 'file-id',
|
||||
file_path: 'documents/file_1.pdf',
|
||||
},
|
||||
},
|
||||
binary: {
|
||||
data: {
|
||||
data: 'test-file',
|
||||
mimeType: 'application/pdf',
|
||||
},
|
||||
},
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
|
||||
Buffer.from('test-file'),
|
||||
'file_1.pdf',
|
||||
'application/pdf',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to application/octet-stream if the mime type cannot be determined', async () => {
|
||||
apiRequestSpy.mockResolvedValueOnce({
|
||||
result: {
|
||||
file_id: 'file-id',
|
||||
file_path: 'documents/file_1.foo',
|
||||
},
|
||||
});
|
||||
apiRequestSpy.mockResolvedValueOnce({
|
||||
body: Buffer.from('test-file'),
|
||||
});
|
||||
executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
|
||||
data: 'test-file',
|
||||
mimeType: 'application/octet-stream',
|
||||
});
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
result: {
|
||||
file_id: 'file-id',
|
||||
file_path: 'documents/file_1.foo',
|
||||
},
|
||||
},
|
||||
binary: {
|
||||
data: {
|
||||
data: 'test-file',
|
||||
mimeType: 'application/octet-stream',
|
||||
},
|
||||
},
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
|
||||
Buffer.from('test-file'),
|
||||
'file_1.foo',
|
||||
'application/octet-stream',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use the provided mime type if it is specified', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((p) => {
|
||||
switch (p) {
|
||||
case 'resource':
|
||||
return 'file';
|
||||
case 'operation':
|
||||
return 'get';
|
||||
case 'download':
|
||||
return true;
|
||||
case 'fileId':
|
||||
return 'file-id';
|
||||
case 'additionalFields':
|
||||
return { mimeType: 'image/jpeg' };
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
apiRequestSpy.mockResolvedValueOnce({
|
||||
result: {
|
||||
file_id: 'file-id',
|
||||
file_path: 'documents/file_1.pdf',
|
||||
},
|
||||
});
|
||||
apiRequestSpy.mockResolvedValueOnce({
|
||||
body: Buffer.from('test-file'),
|
||||
});
|
||||
executeFunctionsMock.helpers.prepareBinaryData.mockResolvedValue({
|
||||
data: 'test-file',
|
||||
mimeType: 'image/jpeg',
|
||||
});
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
result: {
|
||||
file_id: 'file-id',
|
||||
file_path: 'documents/file_1.pdf',
|
||||
},
|
||||
},
|
||||
binary: {
|
||||
data: {
|
||||
data: 'test-file',
|
||||
mimeType: 'image/jpeg',
|
||||
},
|
||||
},
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(executeFunctionsMock.helpers.prepareBinaryData).toHaveBeenCalledWith(
|
||||
Buffer.from('test-file'),
|
||||
'file_1.pdf',
|
||||
'image/jpeg',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertBinaryData usage', () => {
|
||||
beforeEach(() => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName, _) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'message';
|
||||
case 'operation':
|
||||
return 'sendPhoto';
|
||||
case 'binaryData':
|
||||
return true;
|
||||
case 'chatId':
|
||||
return 'chat-id';
|
||||
case 'binaryPropertyName':
|
||||
return 'data';
|
||||
case 'additionalFields.fileName':
|
||||
return '';
|
||||
case 'additionalFields':
|
||||
return {};
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call assertBinaryData with correct parameters', async () => {
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data: {
|
||||
data: 'binary-data',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'photo.jpg',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
executeFunctionsMock.helpers.assertBinaryData.mockReturnValue({
|
||||
data: 'binary-data',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'photo.jpg',
|
||||
});
|
||||
|
||||
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'data');
|
||||
});
|
||||
|
||||
it('should call assertBinaryData for each item with correct index', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName, index) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'message';
|
||||
case 'operation':
|
||||
return 'sendPhoto';
|
||||
case 'binaryData':
|
||||
return true;
|
||||
case 'chatId':
|
||||
return `chat-id-${index}`;
|
||||
case 'binaryPropertyName':
|
||||
return `data${index}`;
|
||||
case 'additionalFields.fileName':
|
||||
return '';
|
||||
case 'additionalFields':
|
||||
return {};
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data0: {
|
||||
data: 'binary-data-0',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'photo0.jpg',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data1: {
|
||||
data: 'binary-data-1',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'photo1.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data2: {
|
||||
data: 'binary-data-2',
|
||||
mimeType: 'image/gif',
|
||||
fileName: 'photo2.gif',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
executeFunctionsMock.helpers.assertBinaryData.mockImplementation(legacyBinaryAccessHelper);
|
||||
|
||||
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledTimes(3);
|
||||
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenNthCalledWith(1, 0, 'data0');
|
||||
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenNthCalledWith(2, 1, 'data1');
|
||||
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenNthCalledWith(3, 2, 'data2');
|
||||
});
|
||||
|
||||
it('should throw error when binary data is missing', async () => {
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
// No binary data
|
||||
},
|
||||
]);
|
||||
|
||||
executeFunctionsMock.helpers.assertBinaryData.mockImplementation(() => {
|
||||
throw new Error('No binary data exists on item!');
|
||||
});
|
||||
|
||||
await expect(node.execute.call(executeFunctionsMock)).rejects.toThrow(
|
||||
'No binary data exists on item!',
|
||||
);
|
||||
|
||||
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'data');
|
||||
});
|
||||
|
||||
it('should throw error when specified binary property does not exist', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'message';
|
||||
case 'operation':
|
||||
return 'sendPhoto';
|
||||
case 'binaryData':
|
||||
return true;
|
||||
case 'chatId':
|
||||
return 'chat-id';
|
||||
case 'binaryPropertyName':
|
||||
return 'nonExistentProperty';
|
||||
case 'additionalFields.fileName':
|
||||
return '';
|
||||
case 'additionalFields':
|
||||
return {};
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data: {
|
||||
data: 'binary-data',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'photo.jpg',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
executeFunctionsMock.helpers.assertBinaryData.mockImplementation(() => {
|
||||
throw new Error("There is no binary data property 'nonExistentProperty' on item!");
|
||||
});
|
||||
|
||||
await expect(node.execute.call(executeFunctionsMock)).rejects.toThrow(
|
||||
"There is no binary data property 'nonExistentProperty' on item!",
|
||||
);
|
||||
|
||||
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(
|
||||
0,
|
||||
'nonExistentProperty',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use fileName from assertBinaryData result when additionalFields.fileName is not provided', async () => {
|
||||
const mockBinaryData = {
|
||||
data: 'binary-data',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'from-binary-data.jpg',
|
||||
};
|
||||
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data: mockBinaryData,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
executeFunctionsMock.helpers.assertBinaryData.mockReturnValue(mockBinaryData);
|
||||
|
||||
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.helpers.assertBinaryData).toHaveBeenCalledWith(0, 'data');
|
||||
expect(apiRequestSpy).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'sendPhoto',
|
||||
{},
|
||||
{},
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({
|
||||
photo: expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
filename: 'from-binary-data.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('message:sendPhoto with binary data', () => {
|
||||
beforeEach(() => {
|
||||
executeFunctionsMock.helpers.assertBinaryData.mockImplementation(legacyBinaryAccessHelper);
|
||||
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName, index) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'message';
|
||||
case 'operation':
|
||||
return 'sendPhoto';
|
||||
case 'binaryData':
|
||||
return true;
|
||||
case 'chatId':
|
||||
return index === 0 ? 'chat-id-0' : index === 1 ? 'chat-id-1' : 'chat-id-2';
|
||||
case 'binaryPropertyName':
|
||||
return index === 0 ? 'data0' : index === 1 ? 'data1' : 'data2';
|
||||
case 'additionalFields.fileName':
|
||||
return index === 0 ? 'photo0.jpg' : index === 1 ? 'photo1.png' : 'photo2.gif';
|
||||
case 'additionalFields':
|
||||
return {};
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should use correct index for binaryPropertyName parameter', async () => {
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data0: {
|
||||
data: 'binary-data-0',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'original0.jpg',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data1: {
|
||||
data: 'binary-data-1',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'original1.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('binaryPropertyName', 0);
|
||||
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('binaryPropertyName', 1);
|
||||
expect(executeFunctionsMock.getNodeParameter).not.toHaveBeenCalledWith(
|
||||
'binaryPropertyName',
|
||||
0,
|
||||
expect.anything(),
|
||||
);
|
||||
expect(executeFunctionsMock.getNodeParameter).not.toHaveBeenCalledWith(
|
||||
'binaryPropertyName',
|
||||
1,
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use correct index for additionalFields.fileName parameter', async () => {
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data0: {
|
||||
data: 'binary-data-0',
|
||||
mimeType: 'image/jpeg',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data1: {
|
||||
data: 'binary-data-1',
|
||||
mimeType: 'image/png',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith(
|
||||
'additionalFields.fileName',
|
||||
0,
|
||||
'',
|
||||
);
|
||||
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith(
|
||||
'additionalFields.fileName',
|
||||
1,
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use correct binary data for each item based on binaryPropertyName index', async () => {
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data0: {
|
||||
data: 'binary-data-0',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'original0.jpg',
|
||||
},
|
||||
wrongData: {
|
||||
data: 'wrong-binary-data',
|
||||
mimeType: 'image/gif',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data1: {
|
||||
data: 'binary-data-1',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'original1.png',
|
||||
},
|
||||
wrongData: {
|
||||
data: 'wrong-binary-data',
|
||||
mimeType: 'image/gif',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'POST',
|
||||
'sendPhoto',
|
||||
{},
|
||||
{},
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({
|
||||
chat_id: 'chat-id-0',
|
||||
photo: expect.objectContaining({
|
||||
value: expect.any(Buffer),
|
||||
options: expect.objectContaining({
|
||||
filename: 'photo0.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'POST',
|
||||
'sendPhoto',
|
||||
{},
|
||||
{},
|
||||
expect.objectContaining({
|
||||
formData: expect.objectContaining({
|
||||
chat_id: 'chat-id-1',
|
||||
photo: expect.objectContaining({
|
||||
value: expect.any(Buffer),
|
||||
options: expect.objectContaining({
|
||||
filename: 'photo1.png',
|
||||
contentType: 'image/png',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to binary fileName when additionalFields.fileName is empty', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName, index) => {
|
||||
switch (paramName) {
|
||||
case 'resource':
|
||||
return 'message';
|
||||
case 'operation':
|
||||
return 'sendPhoto';
|
||||
case 'binaryData':
|
||||
return true;
|
||||
case 'chatId':
|
||||
return index === 0 ? 'chat-id-0' : 'chat-id-1';
|
||||
case 'binaryPropertyName':
|
||||
return index === 0 ? 'data0' : 'data1';
|
||||
case 'additionalFields.fileName':
|
||||
return index === 0 ? '' : 'custom-name.jpg';
|
||||
case 'additionalFields':
|
||||
return {};
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data0: {
|
||||
data: 'binary-data-0',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'fallback-name.jpg',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data1: {
|
||||
data: 'binary-data-1',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'original-name.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
const expectFileName = (index: number, filename: string) => {
|
||||
expect(apiRequestSpy).toHaveBeenNthCalledWith(
|
||||
index,
|
||||
'POST',
|
||||
'sendPhoto',
|
||||
{},
|
||||
{},
|
||||
{
|
||||
formData: expect.objectContaining({
|
||||
photo: expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
filename,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
expectFileName(1, 'fallback-name.jpg');
|
||||
expectFileName(2, 'custom-name.jpg');
|
||||
});
|
||||
|
||||
it('should process different chat IDs for multiple items correctly', async () => {
|
||||
executeFunctionsMock.getInputData.mockReturnValue([
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data0: {
|
||||
data: 'binary-data-0',
|
||||
mimeType: 'image/jpeg',
|
||||
fileName: 'photo0.jpg',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data1: {
|
||||
data: 'binary-data-1',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'photo1.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data2: {
|
||||
data: 'binary-data-2',
|
||||
mimeType: 'image/gif',
|
||||
fileName: 'photo2.gif',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
apiRequestSpy.mockResolvedValue([{ result: { message_id: 123 } }]);
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('chatId', 0);
|
||||
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('chatId', 1);
|
||||
expect(executeFunctionsMock.getNodeParameter).toHaveBeenCalledWith('chatId', 2);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
const expectChatId = (n: number, chatId: string) => {
|
||||
expect(apiRequestSpy).toHaveBeenNthCalledWith(
|
||||
n,
|
||||
'POST',
|
||||
'sendPhoto',
|
||||
{},
|
||||
{},
|
||||
{
|
||||
formData: expect.objectContaining({
|
||||
chat_id: chatId,
|
||||
}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
expectChatId(1, 'chat-id-0');
|
||||
expectChatId(2, 'chat-id-1');
|
||||
expectChatId(3, 'chat-id-2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { type INode, type Workflow } from 'n8n-workflow';
|
||||
|
||||
import { testWebhookTriggerNode } from '@test/nodes/TriggerHelpers';
|
||||
|
||||
import { TelegramTrigger } from '../TelegramTrigger.node';
|
||||
|
||||
jest.mock('../GenericFunctions', () => {
|
||||
const originalModule = jest.requireActual('../GenericFunctions');
|
||||
return {
|
||||
...originalModule,
|
||||
apiRequest: jest.fn(async function (method: string, query: string) {
|
||||
if (method === 'GET' && query.startsWith('getFile')) {
|
||||
return { result: { file_path: 'path/to/file' } };
|
||||
}
|
||||
if (method === 'GET' && !query) {
|
||||
return { body: 'test-file' };
|
||||
}
|
||||
return { result: { file_path: 'path/to/file' } };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('TelegramTrigger', () => {
|
||||
let mockResult: Record<string, object>;
|
||||
|
||||
const binaryData = {
|
||||
fileName: 'mocked-file',
|
||||
mimeType: 'image/png',
|
||||
data: Buffer.from('mocked-data'),
|
||||
};
|
||||
|
||||
const createOptions = ({
|
||||
type,
|
||||
attachment,
|
||||
useChannelPost = false,
|
||||
imageSize = 'small',
|
||||
}: {
|
||||
type: string;
|
||||
attachment: any;
|
||||
useChannelPost?: boolean;
|
||||
imageSize?: string;
|
||||
}) => {
|
||||
const messageField = useChannelPost ? 'channel_post' : 'message';
|
||||
mockResult[messageField] = {
|
||||
chat: { id: 555 },
|
||||
from: { id: 666 },
|
||||
[type]: attachment,
|
||||
};
|
||||
|
||||
return {
|
||||
helpers: {
|
||||
prepareBinaryData: jest.fn().mockResolvedValue(binaryData),
|
||||
},
|
||||
credential: {
|
||||
accessToken: '999999',
|
||||
baseUrl: 'https://api.telegram.org',
|
||||
},
|
||||
workflow: mock<Workflow>({ id: '1', active: true }),
|
||||
node: mock<INode>({
|
||||
id: '2',
|
||||
parameters: {
|
||||
additionalFields: {
|
||||
download: true,
|
||||
chatIds: '555',
|
||||
imageSize,
|
||||
},
|
||||
},
|
||||
}),
|
||||
headerData: {
|
||||
'x-telegram-bot-api-secret-token': '1_2',
|
||||
},
|
||||
bodyData: {
|
||||
[messageField]: {
|
||||
[type]: attachment,
|
||||
chat: { id: 555 },
|
||||
from: { id: 666 },
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockResult = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Webhook', () => {
|
||||
test('should return empty object in download files if attachment is not photo, video, or document', async () => {
|
||||
const options = createOptions({ type: 'text', attachment: 'Hello world!' });
|
||||
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, options);
|
||||
|
||||
expect(responseData).toEqual({ workflowData: [[{ json: mockResult }]] });
|
||||
});
|
||||
|
||||
test('should set the image if it is coming for desktop telegram', async () => {
|
||||
const options = createOptions({
|
||||
type: 'photo',
|
||||
attachment: [{ file_id: 'photo0909' }],
|
||||
imageSize: 'desktop',
|
||||
});
|
||||
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, options);
|
||||
|
||||
expect(responseData).toEqual({
|
||||
workflowData: [[{ json: mockResult, binary: { data: binaryData } }]],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ type: 'photo', attachment: [{ file_id: 'photo0909' }] },
|
||||
{ type: 'video', attachment: { file_id: 'vid666' } },
|
||||
{ type: 'document', attachment: { file_id: '0909' } },
|
||||
])(
|
||||
'should return downloaded files for %s attachments with channel_post',
|
||||
async ({ type, attachment }) => {
|
||||
const options = createOptions({ type, attachment, useChannelPost: true });
|
||||
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, options);
|
||||
|
||||
expect(responseData).toEqual({
|
||||
workflowData: [[{ json: mockResult, binary: { data: binaryData } }]],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ type: 'photo', attachment: [{ file_id: 'photo0909' }] },
|
||||
{ type: 'video', attachment: { file_id: 'vid666' } },
|
||||
{ type: 'document', attachment: { file_id: '0909' } },
|
||||
])(
|
||||
'should return downloaded files for %s attachments with message',
|
||||
async ({ type, attachment }) => {
|
||||
const options = createOptions({ type, attachment });
|
||||
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, options);
|
||||
|
||||
expect(responseData).toEqual({
|
||||
workflowData: [[{ json: mockResult, binary: { data: binaryData } }]],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('should receive a webhook event without downloading files', async () => {
|
||||
mockResult.message = {
|
||||
chat: { id: 555 },
|
||||
from: { id: 666 },
|
||||
};
|
||||
|
||||
const { responseData } = await testWebhookTriggerNode(TelegramTrigger, {
|
||||
workflow: mock<Workflow>({ id: '1', active: true }),
|
||||
node: mock<INode>({
|
||||
id: '2',
|
||||
parameters: {
|
||||
additionalFields: {
|
||||
download: false,
|
||||
chatIds: '555',
|
||||
userIds: '666',
|
||||
},
|
||||
},
|
||||
}),
|
||||
headerData: {
|
||||
'x-telegram-bot-api-secret-token': '1_2',
|
||||
},
|
||||
bodyData: {
|
||||
message: {
|
||||
chat: { id: 555 },
|
||||
from: { id: 666 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(responseData).toEqual({ workflowData: [[{ json: mockResult }]] });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,437 @@
|
||||
export const getChatResponse = {
|
||||
ok: true,
|
||||
result: {
|
||||
id: 123456789,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
type: 'private',
|
||||
active_usernames: ['n8n'],
|
||||
bio: 'Automation',
|
||||
has_private_forwards: true,
|
||||
max_reaction_count: 11,
|
||||
accent_color_id: 3,
|
||||
},
|
||||
};
|
||||
|
||||
export const sendMessageResponse = {
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 40,
|
||||
from: {
|
||||
id: 9876543210,
|
||||
is_bot: true,
|
||||
first_name: '@n8n',
|
||||
username: 'n8n_test_bot',
|
||||
},
|
||||
chat: {
|
||||
id: 123456789,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
type: 'private',
|
||||
},
|
||||
date: 1732960606,
|
||||
text: 'a\n\nThis message was sent automatically with n8n',
|
||||
entities: [
|
||||
{
|
||||
offset: 3,
|
||||
length: 41,
|
||||
type: 'italic',
|
||||
},
|
||||
{
|
||||
offset: 44,
|
||||
length: 3,
|
||||
type: 'text_link',
|
||||
url: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_8c8c5237b8e37b006a7adce87f4369350c58e41f3ca9de16196d3197f69eabcd',
|
||||
},
|
||||
],
|
||||
link_preview_options: {
|
||||
is_disabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const sendMediaGroupResponse = {
|
||||
ok: true,
|
||||
result: [
|
||||
{
|
||||
message_id: 41,
|
||||
from: {
|
||||
id: 9876543210,
|
||||
is_bot: true,
|
||||
first_name: '@n8n',
|
||||
username: 'n8n_test_bot',
|
||||
},
|
||||
chat: {
|
||||
id: 123456789,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
type: 'private',
|
||||
},
|
||||
date: 1732963445,
|
||||
photo: [
|
||||
{
|
||||
file_id:
|
||||
'AgACAgQAAxkDAAMpZ0rsde8lw0E3xttFxGpPdwkExZIAAv21MRvcM11S26tCdFbflv4BAAMCAANzAAM2BA',
|
||||
file_unique_id: 'AQAD_bUxG9wzXVJ4',
|
||||
file_size: 919,
|
||||
width: 90,
|
||||
height: 24,
|
||||
},
|
||||
{
|
||||
file_id:
|
||||
'AgACAgQAAxkDAAMpZ0rsde8lw0E3xttFxGpPdwkExZIAAv21MRvcM11S26tCdFbflv4BAAMCAANtAAM2BA',
|
||||
file_unique_id: 'AQAD_bUxG9wzXVJy',
|
||||
file_size: 6571,
|
||||
width: 320,
|
||||
height: 87,
|
||||
},
|
||||
{
|
||||
file_id:
|
||||
'AgACAgQAAxkDAAMpZ0rsde8lw0E3xttFxGpPdwkExZIAAv21MRvcM11S26tCdFbflv4BAAMCAAN4AAM2BA',
|
||||
file_unique_id: 'AQAD_bUxG9wzXVJ9',
|
||||
file_size: 9639,
|
||||
width: 458,
|
||||
height: 124,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const sendLocationMessageResponse = {
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 42,
|
||||
from: {
|
||||
id: 9876543210,
|
||||
is_bot: true,
|
||||
first_name: '@n8n',
|
||||
username: 'n8n_test_bot',
|
||||
},
|
||||
chat: {
|
||||
id: 123456789,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
type: 'private',
|
||||
},
|
||||
date: 1732963630,
|
||||
reply_to_message: {
|
||||
message_id: 40,
|
||||
from: {
|
||||
id: 9876543210,
|
||||
is_bot: true,
|
||||
first_name: '@n8n',
|
||||
username: 'n8n_test_bot',
|
||||
},
|
||||
chat: {
|
||||
id: 123456789,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
type: 'private',
|
||||
},
|
||||
date: 1732960606,
|
||||
text: 'a\n\nThis message was sent automatically with n8n',
|
||||
entities: [
|
||||
{
|
||||
offset: 3,
|
||||
length: 41,
|
||||
type: 'italic',
|
||||
},
|
||||
{
|
||||
offset: 44,
|
||||
length: 3,
|
||||
type: 'text_link',
|
||||
url: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_8c8c5237b8e37b006a7adce87f4369350c58e41f3ca9de16196d3197f69eabcd',
|
||||
},
|
||||
],
|
||||
link_preview_options: {
|
||||
is_disabled: true,
|
||||
},
|
||||
},
|
||||
location: {
|
||||
latitude: 0.00001,
|
||||
longitude: 0.000003,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const okTrueResponse = {
|
||||
ok: true,
|
||||
result: true,
|
||||
};
|
||||
|
||||
export const sendStickerResponse = {
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 44,
|
||||
from: {
|
||||
id: 9876543210,
|
||||
is_bot: true,
|
||||
first_name: '@n8n',
|
||||
username: 'n8n_test_bot',
|
||||
},
|
||||
chat: {
|
||||
id: 123456789,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
type: 'private',
|
||||
},
|
||||
date: 1732965815,
|
||||
document: {
|
||||
file_name: '1_webp_ll.png',
|
||||
mime_type: 'image/png',
|
||||
thumbnail: {
|
||||
file_id: 'AAMCBAADGQMAAyxnSvW31uMAAWa2AAFl0vD1zqc_3xXeAAIbBwACJ95cUvzVqVKE_cXTAQAHbQADNgQ',
|
||||
file_unique_id: 'AQADGwcAAifeXFJy',
|
||||
file_size: 12534,
|
||||
width: 320,
|
||||
height: 241,
|
||||
},
|
||||
thumb: {
|
||||
file_id: 'AAMCBAADGQMAAyxnSvW31uMAAWa2AAFl0vD1zqc_3xXeAAIbBwACJ95cUvzVqVKE_cXTAQAHbQADNgQ',
|
||||
file_unique_id: 'AQADGwcAAifeXFJy',
|
||||
file_size: 12534,
|
||||
width: 320,
|
||||
height: 241,
|
||||
},
|
||||
file_id: 'BQACAgQAAxkDAAMsZ0r1t9bjAAFmtgABZdLw9c6nP98V3gACGwcAAifeXFL81alShP3F0zYE',
|
||||
file_unique_id: 'AgADGwcAAifeXFI',
|
||||
file_size: 122750,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const editMessageTextResponse = {
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 40,
|
||||
from: {
|
||||
id: 9876543210,
|
||||
is_bot: true,
|
||||
first_name: '@n8n',
|
||||
username: 'n8n_test_bot',
|
||||
},
|
||||
chat: {
|
||||
id: 123456789,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
type: 'private',
|
||||
},
|
||||
date: 1732960606,
|
||||
edit_date: 1732967008,
|
||||
text: 'test',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{
|
||||
text: 'foo',
|
||||
callback_data: 'callback',
|
||||
},
|
||||
{
|
||||
text: 'n8n',
|
||||
url: 'https://n8n.io/',
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const chatAdministratorsResponse = {
|
||||
ok: true,
|
||||
result: [
|
||||
{
|
||||
user: {
|
||||
id: 9876543210,
|
||||
is_bot: true,
|
||||
first_name: '@n8n',
|
||||
username: 'n8n_test_bot',
|
||||
},
|
||||
status: 'administrator',
|
||||
can_be_edited: false,
|
||||
can_manage_chat: true,
|
||||
can_change_info: true,
|
||||
can_post_messages: true,
|
||||
can_edit_messages: true,
|
||||
can_delete_messages: true,
|
||||
can_invite_users: true,
|
||||
can_restrict_members: true,
|
||||
can_promote_members: false,
|
||||
can_manage_video_chats: true,
|
||||
can_post_stories: true,
|
||||
can_edit_stories: true,
|
||||
can_delete_stories: true,
|
||||
is_anonymous: false,
|
||||
can_manage_voice_chats: true,
|
||||
},
|
||||
{
|
||||
user: {
|
||||
id: 123456789,
|
||||
is_bot: false,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
language_code: 'en',
|
||||
},
|
||||
status: 'creator',
|
||||
is_anonymous: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const sendAnimationMessageResponse = {
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 45,
|
||||
from: {
|
||||
id: 9876543210,
|
||||
is_bot: true,
|
||||
first_name: '@n8n',
|
||||
username: 'n8n_test_bot',
|
||||
},
|
||||
chat: {
|
||||
id: 123456789,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
type: 'private',
|
||||
},
|
||||
date: 1732968868,
|
||||
animation: {
|
||||
file_name: 'Telegram---Opening-Image.gif.mp4',
|
||||
mime_type: 'video/mp4',
|
||||
duration: 6,
|
||||
width: 320,
|
||||
height: 320,
|
||||
thumbnail: {
|
||||
file_id: 'AAMCBAADGQMAAy1nSwGkq99SDYaaS1VR0EMAAUrOw1cAAiYEAALzCVxR7jIYS8d3HycBAAdtAAM2BA',
|
||||
file_unique_id: 'AQADJgQAAvMJXFFy',
|
||||
file_size: 36480,
|
||||
width: 320,
|
||||
height: 320,
|
||||
},
|
||||
thumb: {
|
||||
file_id: 'AAMCBAADGQMAAy1nSwGkq99SDYaaS1VR0EMAAUrOw1cAAiYEAALzCVxR7jIYS8d3HycBAAdtAAM2BA',
|
||||
file_unique_id: 'AQADJgQAAvMJXFFy',
|
||||
file_size: 36480,
|
||||
width: 320,
|
||||
height: 320,
|
||||
},
|
||||
file_id: 'CgACAgQAAxkDAAMtZ0sBpKvfUg2GmktVUdBDAAFKzsNXAAImBAAC8wlcUe4yGEvHdx8nNgQ',
|
||||
file_unique_id: 'AgADJgQAAvMJXFE',
|
||||
file_size: 309245,
|
||||
},
|
||||
document: {
|
||||
file_name: 'Telegram---Opening-Image.gif.mp4',
|
||||
mime_type: 'video/mp4',
|
||||
thumbnail: {
|
||||
file_id: 'AAMCBAADGQMAAy1nSwGkq99SDYaaS1VR0EMAAUrOw1cAAiYEAALzCVxR7jIYS8d3HycBAAdtAAM2BA',
|
||||
file_unique_id: 'AQADJgQAAvMJXFFy',
|
||||
file_size: 36480,
|
||||
width: 320,
|
||||
height: 320,
|
||||
},
|
||||
thumb: {
|
||||
file_id: 'AAMCBAADGQMAAy1nSwGkq99SDYaaS1VR0EMAAUrOw1cAAiYEAALzCVxR7jIYS8d3HycBAAdtAAM2BA',
|
||||
file_unique_id: 'AQADJgQAAvMJXFFy',
|
||||
file_size: 36480,
|
||||
width: 320,
|
||||
height: 320,
|
||||
},
|
||||
file_id: 'CgACAgQAAxkDAAMtZ0sBpKvfUg2GmktVUdBDAAFKzsNXAAImBAAC8wlcUe4yGEvHdx8nNgQ',
|
||||
file_unique_id: 'AgADJgQAAvMJXFE',
|
||||
file_size: 309245,
|
||||
},
|
||||
caption: 'Animation',
|
||||
},
|
||||
};
|
||||
|
||||
export const sendAudioResponse = {
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 46,
|
||||
from: {
|
||||
id: 9876543210,
|
||||
is_bot: true,
|
||||
first_name: '@n8n',
|
||||
username: 'n8n_test_bot',
|
||||
},
|
||||
chat: {
|
||||
id: 123456789,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
type: 'private',
|
||||
},
|
||||
date: 1732969291,
|
||||
audio: {
|
||||
duration: 3,
|
||||
file_name: 'sample-3s.mp3',
|
||||
mime_type: 'audio/mpeg',
|
||||
file_id: 'CQACAgQAAxkDAAMuZ0sDSxCh3hW89NQa-eTpxKioqGAAAjsEAAIBCU1SGtsPA4N9TSo2BA',
|
||||
file_unique_id: 'AgADOwQAAgEJTVI',
|
||||
file_size: 52079,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getMemberResponse = {
|
||||
ok: true,
|
||||
result: {
|
||||
user: {
|
||||
id: 123456789,
|
||||
is_bot: false,
|
||||
first_name: 'Nathan',
|
||||
last_name: 'W',
|
||||
username: 'n8n',
|
||||
language_code: 'en',
|
||||
},
|
||||
status: 'creator',
|
||||
is_anonymous: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const sendMessageWithBinaryDataAndReplyMarkupResponse = {
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: 123,
|
||||
from: {
|
||||
id: 1234578901,
|
||||
is_bot: true,
|
||||
first_name: 'TestBot',
|
||||
username: 'TestBot',
|
||||
},
|
||||
chat: {
|
||||
id: 987654321,
|
||||
first_name: 'Some',
|
||||
last_name: 'Guy',
|
||||
username: 'SomeGuy',
|
||||
type: 'private',
|
||||
},
|
||||
date: 1750195377,
|
||||
document: {
|
||||
file_name: 'file.json',
|
||||
mime_type: 'application/json',
|
||||
file_id: 'BQACAgIAAxkDAANFaFHcsX7_6XEYxKTw3Y93hBKxdPEAAm1_AAJ3NpBKL3xbHXAyvIU2BA',
|
||||
file_unique_id: 'AgADbX8AAnc2kEo',
|
||||
file_size: 24,
|
||||
},
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{
|
||||
text: 'Test Button',
|
||||
callback_data: '123',
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"name": "Telegram Binary Data and Reply Markup",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-700, 160],
|
||||
"id": "6acb0d3b-6f5e-43dd-adeb-152ab4e9cc90",
|
||||
"name": "When clicking ‘Test workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "sendDocument",
|
||||
"chatId": "123456789",
|
||||
"binaryData": true,
|
||||
"replyMarkup": "inlineKeyboard",
|
||||
"inlineKeyboard": {
|
||||
"rows": [
|
||||
{
|
||||
"row": {
|
||||
"buttons": [
|
||||
{
|
||||
"text": "Test Button",
|
||||
"additionalFields": {
|
||||
"callback_data": "123"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"additionalFields": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.telegram",
|
||||
"typeVersion": 1.2,
|
||||
"position": [-40, 160],
|
||||
"id": "71580477-ff66-487d-9762-4bdf5cc0b5a9",
|
||||
"name": "Send a document",
|
||||
"webhookId": "bea3ccc9-bda6-4353-904e-bff92d608457",
|
||||
"credentials": {
|
||||
"telegramApi": {
|
||||
"id": "HcIHBfGmAEOgtHgq",
|
||||
"name": "Telegram account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "toJson",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.convertToFile",
|
||||
"typeVersion": 1.1,
|
||||
"position": [-260, 160],
|
||||
"id": "deeef2c2-dd75-4719-9288-e8612d67c09f",
|
||||
"name": "Convert to File"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "2c5d3b18-1876-49a8-bf71-02cfebf2e5f3",
|
||||
"name": "data",
|
||||
"value": "lorem ipsum",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [-480, 160],
|
||||
"id": "79bf86ea-d10d-4e00-b251-b4126554d993",
|
||||
"name": "Edit Fields"
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Send a document": [
|
||||
{
|
||||
"json": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"message_id": 123,
|
||||
"from": {
|
||||
"id": 1234578901,
|
||||
"is_bot": true,
|
||||
"first_name": "TestBot",
|
||||
"username": "TestBot"
|
||||
},
|
||||
"chat": {
|
||||
"id": 987654321,
|
||||
"first_name": "Some",
|
||||
"last_name": "Guy",
|
||||
"username": "SomeGuy",
|
||||
"type": "private"
|
||||
},
|
||||
"date": 1750195377,
|
||||
"document": {
|
||||
"file_name": "file.json",
|
||||
"mime_type": "application/json",
|
||||
"file_id": "BQACAgIAAxkDAANFaFHcsX7_6XEYxKTw3Y93hBKxdPEAAm1_AAJ3NpBKL3xbHXAyvIU2BA",
|
||||
"file_unique_id": "AgADbX8AAnc2kEo",
|
||||
"file_size": 24
|
||||
},
|
||||
"reply_markup": {
|
||||
"inline_keyboard": [
|
||||
[
|
||||
{
|
||||
"text": "Test Button",
|
||||
"callback_data": "123"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Convert to File": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send a document",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Convert to File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "007f0573-7089-4353-9d41-30d705b432ed",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "e115be144a6a5547dbfca93e774dfffa178aa94a181854c13e2ce5e14d195b2e"
|
||||
},
|
||||
"id": "6axpOZWb9wBsnrBS",
|
||||
"tags": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
getChatResponse,
|
||||
sendMediaGroupResponse,
|
||||
sendMessageResponse,
|
||||
sendLocationMessageResponse,
|
||||
okTrueResponse,
|
||||
sendStickerResponse,
|
||||
editMessageTextResponse,
|
||||
chatAdministratorsResponse,
|
||||
sendAnimationMessageResponse,
|
||||
sendAudioResponse,
|
||||
getMemberResponse,
|
||||
sendMessageWithBinaryDataAndReplyMarkupResponse,
|
||||
} from './apiResponses';
|
||||
|
||||
describe('Telegram', () => {
|
||||
const credentials = {
|
||||
telegramApi: {
|
||||
accessToken: 'testToken',
|
||||
baseUrl: 'https://api.telegram.org',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Run Telegram workflow', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock(credentials.telegramApi.baseUrl);
|
||||
|
||||
mock.post('/bottestToken/getChat').reply(200, getChatResponse);
|
||||
mock.post('/bottestToken/getChat').reply(404, { error: 'Chat not found' });
|
||||
mock.post('/bottestToken/sendMessage').reply(200, sendMessageResponse);
|
||||
mock.post('/bottestToken/sendMediaGroup').reply(200, sendMediaGroupResponse);
|
||||
mock.post('/bottestToken/sendLocation').reply(200, sendLocationMessageResponse);
|
||||
mock.post('/bottestToken/deleteMessage').reply(200, okTrueResponse);
|
||||
mock.post('/bottestToken/pinChatMessage').reply(200, okTrueResponse);
|
||||
mock.post('/bottestToken/setChatDescription').reply(200, okTrueResponse);
|
||||
mock.post('/bottestToken/setChatTitle').reply(200, okTrueResponse);
|
||||
mock.post('/bottestToken/unpinChatMessage').reply(200, okTrueResponse);
|
||||
mock.post('/bottestToken/sendChatAction').reply(200, okTrueResponse);
|
||||
mock.post('/bottestToken/leaveChat').reply(200, okTrueResponse);
|
||||
mock.post('/bottestToken/sendSticker').reply(200, sendStickerResponse);
|
||||
mock.post('/bottestToken/editMessageText').reply(200, editMessageTextResponse);
|
||||
mock.post('/bottestToken/getChatAdministrators').reply(200, chatAdministratorsResponse);
|
||||
mock.post('/bottestToken/sendAnimation').reply(200, sendAnimationMessageResponse);
|
||||
mock.post('/bottestToken/sendAudio').reply(200, sendAudioResponse);
|
||||
mock.post('/bottestToken/getChatMember').reply(200, getMemberResponse);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({ credentials, workflowFiles: ['workflow.json'] });
|
||||
});
|
||||
|
||||
describe('Binary Data and Reply Markup', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock(credentials.telegramApi.baseUrl);
|
||||
mock
|
||||
.post('/bottestToken/sendDocument')
|
||||
.reply(200, sendMessageWithBinaryDataAndReplyMarkupResponse);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({ credentials, workflowFiles: ['binaryData.workflow.json'] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { MockProxy } from 'jest-mock-extended';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { type INode, SEND_AND_WAIT_OPERATION, type IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import * as genericFunctions from '../../GenericFunctions';
|
||||
import { Telegram } from '../../Telegram.node';
|
||||
|
||||
jest.mock('../../GenericFunctions', () => {
|
||||
const originalModule = jest.requireActual('../../GenericFunctions');
|
||||
return {
|
||||
...originalModule,
|
||||
apiRequest: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Test Telegram, message => sendAndWait', () => {
|
||||
let telegram: Telegram;
|
||||
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
telegram = new Telegram();
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should send message and put execution to wait', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
//node
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(SEND_AND_WAIT_OPERATION);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(false);
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>());
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
|
||||
//createSendAndWaitMessageBody
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('chatID');
|
||||
|
||||
//getSendAndWaitConfig
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my message');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('my subject');
|
||||
mockExecuteFunctions.getSignedResumeUrl.mockReturnValue(
|
||||
'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
);
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); // approvalOptions
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); // options
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('approval');
|
||||
|
||||
// configureWaitTillDate
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({}); //options.limitWaitTime.values
|
||||
|
||||
const result = await telegram.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([items]);
|
||||
expect(genericFunctions.apiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(genericFunctions.apiRequest).toHaveBeenCalledWith('POST', 'sendMessage', {
|
||||
chat_id: 'chatID',
|
||||
disable_web_page_preview: true,
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{
|
||||
text: 'Approve',
|
||||
url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc',
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
text: 'my message\n\n_This message was sent automatically with _[n8n](https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_instanceId)',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
type ICredentialDataDecryptedObject,
|
||||
type IDataObject,
|
||||
type IWebhookFunctions,
|
||||
type IWebhookResponseData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { apiRequest, getImageBySize } from '../GenericFunctions';
|
||||
import { type IEvent } from '../IEvent';
|
||||
|
||||
export const downloadFile = async (
|
||||
webhookFunctions: IWebhookFunctions,
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
bodyData: IEvent,
|
||||
additionalFields: IDataObject,
|
||||
): Promise<IWebhookResponseData> => {
|
||||
let imageSize = 'large';
|
||||
|
||||
let key: 'message' | 'channel_post' = 'message';
|
||||
|
||||
if (bodyData.channel_post) {
|
||||
key = 'channel_post';
|
||||
}
|
||||
|
||||
if (
|
||||
(bodyData[key]?.photo && Array.isArray(bodyData[key]?.photo)) ||
|
||||
bodyData[key]?.document ||
|
||||
bodyData[key]?.video
|
||||
) {
|
||||
if (additionalFields.imageSize) {
|
||||
imageSize = additionalFields.imageSize as string;
|
||||
}
|
||||
|
||||
let fileId;
|
||||
|
||||
if (bodyData[key]?.photo) {
|
||||
let image = getImageBySize(bodyData[key]?.photo as IDataObject[], imageSize) as IDataObject;
|
||||
|
||||
// When the image is sent from the desktop app telegram does not resize the image
|
||||
// So return the only image available
|
||||
// Basically the Image Size parameter would work just when the images comes from the mobile app
|
||||
if (image === undefined) {
|
||||
image = bodyData[key]!.photo![0];
|
||||
}
|
||||
|
||||
fileId = image.file_id;
|
||||
} else if (bodyData[key]?.video) {
|
||||
fileId = bodyData[key]?.video?.file_id;
|
||||
} else {
|
||||
fileId = bodyData[key]?.document?.file_id;
|
||||
}
|
||||
|
||||
const {
|
||||
result: { file_path },
|
||||
} = await apiRequest.call(webhookFunctions, 'GET', `getFile?file_id=${fileId}`, {});
|
||||
|
||||
const file = await apiRequest.call(
|
||||
webhookFunctions,
|
||||
'GET',
|
||||
'',
|
||||
{},
|
||||
{},
|
||||
{
|
||||
json: false,
|
||||
encoding: null,
|
||||
uri: `${credentials.baseUrl}/file/bot${credentials.accessToken}/${file_path}`,
|
||||
resolveWithFullResponse: true,
|
||||
},
|
||||
);
|
||||
|
||||
const data = Buffer.from(file.body as string);
|
||||
|
||||
const fileName = file_path.split('/').pop();
|
||||
|
||||
const binaryData = await webhookFunctions.helpers.prepareBinaryData(
|
||||
data as unknown as Buffer,
|
||||
fileName as string,
|
||||
);
|
||||
|
||||
return {
|
||||
workflowData: [
|
||||
[
|
||||
{
|
||||
json: bodyData as unknown as IDataObject,
|
||||
binary: {
|
||||
data: binaryData,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
Reference in New Issue
Block a user