mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-02 07:01:30 -08:00
feat(Telegram Node): New operation sendAndWait (#12771)
This commit is contained in:
parent
5b760e7f7f
commit
2c58d47f8e
|
@ -13,6 +13,7 @@ import { labelFields, labelOperations } from './LabelDescription';
|
|||
import { getGmailAliases, getLabels, getThreadMessages } from './loadOptions';
|
||||
import { messageFields, messageOperations } from './MessageDescription';
|
||||
import { threadFields, threadOperations } from './ThreadDescription';
|
||||
import { sendAndWaitWebhooks } from '../../../../utils/sendAndWait/descriptions';
|
||||
import type { IEmail } from '../../../../utils/sendAndWait/interfaces';
|
||||
import {
|
||||
configureWaitTillDate,
|
||||
|
@ -68,26 +69,7 @@ const versionDescription: INodeTypeDescription = {
|
|||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'GET',
|
||||
responseMode: 'onReceived',
|
||||
responseData: '',
|
||||
path: '={{ $nodeId }}',
|
||||
restartWebhook: true,
|
||||
isFullPath: true,
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
responseData: '',
|
||||
path: '={{ $nodeId }}',
|
||||
restartWebhook: true,
|
||||
isFullPath: true,
|
||||
},
|
||||
],
|
||||
webhooks: sendAndWaitWebhooks,
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as folder from './folder';
|
|||
import * as folderMessage from './folderMessage';
|
||||
import * as message from './message';
|
||||
import * as messageAttachment from './messageAttachment';
|
||||
import { sendAndWaitWebhooks } from '../../../../../utils/sendAndWait/descriptions';
|
||||
|
||||
export const description: INodeTypeDescription = {
|
||||
displayName: 'Microsoft Outlook',
|
||||
|
@ -30,26 +31,7 @@ export const description: INodeTypeDescription = {
|
|||
required: true,
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'GET',
|
||||
responseMode: 'onReceived',
|
||||
responseData: '',
|
||||
path: '={{ $nodeId }}',
|
||||
restartWebhook: true,
|
||||
isFullPath: true,
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
responseData: '',
|
||||
path: '={{ $nodeId }}',
|
||||
restartWebhook: true,
|
||||
isFullPath: true,
|
||||
},
|
||||
],
|
||||
webhooks: sendAndWaitWebhooks,
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
|
|
|
@ -41,6 +41,7 @@ import { reactionFields, reactionOperations } from './ReactionDescription';
|
|||
import { starFields, starOperations } from './StarDescription';
|
||||
import { userFields, userOperations } from './UserDescription';
|
||||
import { userGroupFields, userGroupOperations } from './UserGroupDescription';
|
||||
import { sendAndWaitWebhooks } from '../../../utils/sendAndWait/descriptions';
|
||||
import {
|
||||
configureWaitTillDate,
|
||||
getSendAndWaitProperties,
|
||||
|
@ -80,26 +81,7 @@ export class SlackV2 implements INodeType {
|
|||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'GET',
|
||||
responseMode: 'onReceived',
|
||||
responseData: '',
|
||||
path: '={{ $nodeId }}',
|
||||
restartWebhook: true,
|
||||
isFullPath: true,
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
responseData: '',
|
||||
path: '={{ $nodeId }}',
|
||||
restartWebhook: true,
|
||||
isFullPath: true,
|
||||
},
|
||||
],
|
||||
webhooks: sendAndWaitWebhooks,
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
|
|
|
@ -10,6 +10,8 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { getSendAndWaitConfig } from '../../utils/sendAndWait/utils';
|
||||
|
||||
// Interface in n8n
|
||||
export interface IMarkupKeyboard {
|
||||
rows?: IMarkupKeyboardRow[];
|
||||
|
@ -252,3 +254,36 @@ export function getSecretToken(this: IHookFunctions | IWebhookFunctions) {
|
|||
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;
|
||||
|
||||
const instanceId = context.getInstanceId();
|
||||
const attributionText = 'This message was sent automatically with ';
|
||||
const link = `https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=${encodeURIComponent(
|
||||
'n8n-nodes-base.telegram',
|
||||
)}${instanceId ? '_' + 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: `${config.url}?approved=${option.value}`,
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return body;
|
||||
}
|
||||
|
|
|
@ -6,11 +6,27 @@ import type {
|
|||
INodeTypeDescription,
|
||||
IHttpRequestMethods,
|
||||
} from 'n8n-workflow';
|
||||
import { BINARY_ENCODING, NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
SEND_AND_WAIT_OPERATION,
|
||||
NodeConnectionType,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
import type { Readable } from 'stream';
|
||||
|
||||
import { addAdditionalFields, apiRequest, getPropertyName } from './GenericFunctions';
|
||||
import {
|
||||
addAdditionalFields,
|
||||
apiRequest,
|
||||
createSendAndWaitMessageBody,
|
||||
getPropertyName,
|
||||
} from './GenericFunctions';
|
||||
import { appendAttributionOption } from '../../utils/descriptions';
|
||||
import { sendAndWaitWebhooks } from '../../utils/sendAndWait/descriptions';
|
||||
import {
|
||||
configureWaitTillDate,
|
||||
getSendAndWaitProperties,
|
||||
sendAndWaitWebhook,
|
||||
} from '../../utils/sendAndWait/utils';
|
||||
|
||||
export class Telegram implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
@ -33,6 +49,7 @@ export class Telegram implements INodeType {
|
|||
required: true,
|
||||
},
|
||||
],
|
||||
webhooks: sendAndWaitWebhooks,
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
|
@ -263,6 +280,12 @@ export class Telegram implements INodeType {
|
|||
description: 'Send a text message',
|
||||
action: 'Send a text message',
|
||||
},
|
||||
{
|
||||
name: 'Send and Wait for Response',
|
||||
value: SEND_AND_WAIT_OPERATION,
|
||||
description: 'Send a message and wait for response',
|
||||
action: 'Send message and wait for response',
|
||||
},
|
||||
{
|
||||
name: 'Send Photo',
|
||||
value: 'sendPhoto',
|
||||
|
@ -1735,9 +1758,31 @@ export class Telegram implements INodeType {
|
|||
},
|
||||
],
|
||||
},
|
||||
...getSendAndWaitProperties(
|
||||
[
|
||||
{
|
||||
displayName: 'Chat ID',
|
||||
name: 'chatId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
description:
|
||||
'Unique identifier for the target chat or username of the target channel (in the format @channelusername). To find your chat ID ask @get_id_bot.',
|
||||
},
|
||||
],
|
||||
'message',
|
||||
undefined,
|
||||
{
|
||||
noButtonStyle: true,
|
||||
defaultApproveLabel: '✅ Approve',
|
||||
defaultDisapproveLabel: '❌ Decline',
|
||||
},
|
||||
).filter((p) => p.name !== 'subject'),
|
||||
],
|
||||
};
|
||||
|
||||
webhook = sendAndWaitWebhook;
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
@ -1757,6 +1802,17 @@ export class Telegram implements INodeType {
|
|||
const nodeVersion = this.getNode().typeVersion;
|
||||
const instanceId = this.getInstanceId();
|
||||
|
||||
if (resource === 'message' && operation === SEND_AND_WAIT_OPERATION) {
|
||||
body = createSendAndWaitMessageBody(this);
|
||||
|
||||
await apiRequest.call(this, 'POST', 'sendMessage', body);
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
await this.putExecutionToWait(waitTill);
|
||||
return [this.getInputData()];
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
// Reset all values
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
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.evaluateExpression.mockReturnValueOnce('http://localhost/waiting-webhook');
|
||||
mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('nodeID');
|
||||
mockExecuteFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
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' }],
|
||||
],
|
||||
},
|
||||
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)',
|
||||
});
|
||||
});
|
||||
});
|
22
packages/nodes-base/utils/sendAndWait/descriptions.ts
Normal file
22
packages/nodes-base/utils/sendAndWait/descriptions.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import type { IWebhookDescription } from 'n8n-workflow';
|
||||
|
||||
export const sendAndWaitWebhooks: IWebhookDescription[] = [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'GET',
|
||||
responseMode: 'onReceived',
|
||||
responseData: '',
|
||||
path: '={{ $nodeId }}',
|
||||
restartWebhook: true,
|
||||
isFullPath: true,
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
responseData: '',
|
||||
path: '={{ $nodeId }}',
|
||||
restartWebhook: true,
|
||||
isFullPath: true,
|
||||
},
|
||||
];
|
|
@ -137,6 +137,11 @@ export function getSendAndWaitProperties(
|
|||
targetProperties: INodeProperties[],
|
||||
resource: string = 'message',
|
||||
additionalProperties: INodeProperties[] = [],
|
||||
options?: {
|
||||
noButtonStyle?: boolean;
|
||||
defaultApproveLabel?: string;
|
||||
defaultDisapproveLabel?: string;
|
||||
},
|
||||
) {
|
||||
const buttonStyle: INodeProperties = {
|
||||
displayName: 'Button Style',
|
||||
|
@ -154,6 +159,77 @@ export function getSendAndWaitProperties(
|
|||
},
|
||||
],
|
||||
};
|
||||
const approvalOptionsValues = [
|
||||
{
|
||||
displayName: 'Type of Approval',
|
||||
name: 'approvalType',
|
||||
type: 'options',
|
||||
placeholder: 'Add option',
|
||||
default: 'single',
|
||||
options: [
|
||||
{
|
||||
name: 'Approve Only',
|
||||
value: 'single',
|
||||
},
|
||||
{
|
||||
name: 'Approve and Disapprove',
|
||||
value: 'double',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Approve Button Label',
|
||||
name: 'approveLabel',
|
||||
type: 'string',
|
||||
default: options?.defaultApproveLabel || 'Approve',
|
||||
displayOptions: {
|
||||
show: {
|
||||
approvalType: ['single', 'double'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...[
|
||||
options?.noButtonStyle
|
||||
? ({} as INodeProperties)
|
||||
: {
|
||||
...buttonStyle,
|
||||
displayName: 'Approve Button Style',
|
||||
name: 'buttonApprovalStyle',
|
||||
displayOptions: {
|
||||
show: {
|
||||
approvalType: ['single', 'double'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
displayName: 'Disapprove Button Label',
|
||||
name: 'disapproveLabel',
|
||||
type: 'string',
|
||||
default: options?.defaultDisapproveLabel || 'Decline',
|
||||
displayOptions: {
|
||||
show: {
|
||||
approvalType: ['double'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...[
|
||||
options?.noButtonStyle
|
||||
? ({} as INodeProperties)
|
||||
: {
|
||||
...buttonStyle,
|
||||
displayName: 'Disapprove Button Style',
|
||||
name: 'buttonDisapprovalStyle',
|
||||
default: 'secondary',
|
||||
displayOptions: {
|
||||
show: {
|
||||
approvalType: ['double'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
].filter((p) => Object.keys(p).length) as INodeProperties[];
|
||||
|
||||
const sendAndWait: INodeProperties[] = [
|
||||
...targetProperties,
|
||||
{
|
||||
|
@ -216,68 +292,7 @@ export function getSendAndWaitProperties(
|
|||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Type of Approval',
|
||||
name: 'approvalType',
|
||||
type: 'options',
|
||||
placeholder: 'Add option',
|
||||
default: 'single',
|
||||
options: [
|
||||
{
|
||||
name: 'Approve Only',
|
||||
value: 'single',
|
||||
},
|
||||
{
|
||||
name: 'Approve and Disapprove',
|
||||
value: 'double',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Approve Button Label',
|
||||
name: 'approveLabel',
|
||||
type: 'string',
|
||||
default: 'Approve',
|
||||
displayOptions: {
|
||||
show: {
|
||||
approvalType: ['single', 'double'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...buttonStyle,
|
||||
displayName: 'Approve Button Style',
|
||||
name: 'buttonApprovalStyle',
|
||||
displayOptions: {
|
||||
show: {
|
||||
approvalType: ['single', 'double'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Disapprove Button Label',
|
||||
name: 'disapproveLabel',
|
||||
type: 'string',
|
||||
default: 'Decline',
|
||||
displayOptions: {
|
||||
show: {
|
||||
approvalType: ['double'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...buttonStyle,
|
||||
displayName: 'Disapprove Button Style',
|
||||
name: 'buttonDisapprovalStyle',
|
||||
default: 'secondary',
|
||||
displayOptions: {
|
||||
show: {
|
||||
approvalType: ['double'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
values: approvalOptionsValues,
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
|
|
Loading…
Reference in a new issue