Add Phantombuster Node (#1274)

*  Phantombuster Node

*  Small improvements

*  Small fix

*  Improvements to Phantombuster Node

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Ricardo Espinoza 2021-01-02 14:16:18 -05:00 committed by GitHub
parent 29107f130f
commit 403f1009a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 801 additions and 0 deletions

View file

@ -0,0 +1,18 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class PhantombusterApi implements ICredentialType {
name = 'phantombusterApi';
displayName = 'Phantombuster API';
documentationUrl = 'phantombuster';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -0,0 +1,451 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const agentOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'agent',
],
},
},
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete an agent by id.',
},
{
name: 'Get',
value: 'get',
description: 'Get an agent by id.',
},
{
name: 'Get All',
value: 'getAll',
description: `Get all agents of the current user's organization.`,
},
{
name: 'Get Output',
value: 'getOutput',
description: 'Get the output of the most recent container of an agent.',
},
{
name: 'Launch',
value: 'launch',
description: 'Add an agent to the launch queue.',
},
],
default: 'launch',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const agentFields = [
/* -------------------------------------------------------------------------- */
/* agent:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Agent',
name: 'agentId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAgents',
},
required: true,
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'agent',
],
},
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* agent:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Agent ID',
name: 'agentId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'agent',
],
},
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* agent:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'agent',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'agent',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 50,
},
default: 25,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* agent:getOutput */
/* -------------------------------------------------------------------------- */
{
displayName: 'Agent',
name: 'agentId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAgents',
},
required: true,
displayOptions: {
show: {
operation: [
'getOutput',
],
resource: [
'agent',
],
},
},
default: '',
},
{
displayName: 'Resolve Data',
name: 'resolveData',
type: 'boolean',
default: true,
displayOptions: {
show: {
operation: [
'getOutput',
],
resource: [
'agent',
],
},
},
description: 'By default the outpout is presented as string. If this option gets activated it<br />will resolve the data automatically.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'agent',
],
operation: [
'getOutput',
],
},
},
default: {},
options: [
{
displayName: 'Prev Container ID',
name: 'prevContainerId',
type: 'string',
default: '',
description: `If set, the output will be retrieved from the container after the specified previous container id.`,
},
{
displayName: 'Prev Status',
name: 'prevStatus',
type: 'options',
options: [
{
name: 'Starting',
value: 'starting',
},
{
name: 'Running',
value: 'running',
},
{
name: 'Finished',
value: 'finished',
},
{
name: 'Unknown',
value: 'unknown',
},
{
name: 'Launch Error',
value: 'lauch error',
},
{
name: 'Never Launched',
value: 'never launched',
},
],
default: '',
description: 'If set, allows to define which status was previously retrieved on user-side.',
},
{
displayName: 'Pre Runtime Event Index',
name: 'prevRuntimeEventIndex',
type: 'number',
default: 0,
description: `If set, the container's runtime events will be returned in the response starting from the provided previous runtime event index.`,
},
],
},
/* -------------------------------------------------------------------------- */
/* agent:launch */
/* -------------------------------------------------------------------------- */
{
displayName: 'Agent',
name: 'agentId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAgents',
},
required: true,
displayOptions: {
show: {
operation: [
'launch',
],
resource: [
'agent',
],
},
},
default: '',
},
{
displayName: 'Resolve Data',
name: 'resolveData',
type: 'boolean',
default: true,
displayOptions: {
show: {
operation: [
'launch',
],
resource: [
'agent',
],
},
},
description: 'By default the launch just include the container ID. If this option gets activated it<br />will resolve the data automatically.',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
operation: [
'launch',
],
resource: [
'agent',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'agent',
],
operation: [
'launch',
],
},
},
default: {},
options: [
{
displayName: 'Arguments (JSON)',
name: 'argumentsJson',
type: 'json',
displayOptions: {
show: {
'/jsonParameters': [
true,
],
},
},
default: '',
description: 'Agent argument. Can either be a JSON string or a plain object. The argument can be retrieved with buster.argument in the agents script.',
},
{
displayName: 'Arguments',
name: 'argumentsUi',
placeholder: 'Add Argument',
type: 'fixedCollection',
default: '',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
options: [
{
name: 'argumentValues',
displayName: 'Argument',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'Name of the argument key to add.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the argument key.',
},
],
},
],
},
{
displayName: 'Bonus Argument',
name: 'bonusArgumentUi',
placeholder: 'Add Bonus Argument',
type: 'fixedCollection',
default: '',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
options: [
{
name: 'bonusArgumentValue',
displayName: 'Bonus Argument',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'Name of the argument key to add.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the argument key.',
},
],
},
],
},
{
displayName: 'Bonus Argument (JSON)',
name: 'bonusArgumentJson',
type: 'string',
displayOptions: {
show: {
'/jsonParameters': [
true,
],
},
},
default: '',
description: `Agent bonus argument. Can either be a JSON string or a plain object. This bonus argument is single-use, it will only be used for the current launch. If present, it will be merged with the original argument, resulting in an effective argument that can be retrieved with buster.argument in the agents script.`,
},
{
displayName: 'Manual Launch',
name: 'manualLaunch',
type: 'boolean',
default: false,
description: 'If set, the agent will be considered as "launched manually".',
},
{
displayName: 'Max Instance Count',
name: 'maxInstanceCount',
type: 'number',
default: 0,
description: 'If set, the agent will only be launched if the number of already running instances is below the specified number.',
},
{
displayName: 'Save Argument',
name: 'saveArgument',
type: 'string',
default: '',
description: 'If true, argument will be saved as the default launch options for the agent.',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,56 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function phantombusterApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('phantombusterApi') as IDataObject;
const options: OptionsWithUri = {
headers: {
'X-Phantombuster-Key': credentials.apiKey,
},
method,
body,
qs,
uri: `https://api.phantombuster.com/api/v2${path}`,
json: true,
};
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
//@ts-ignore
return await this.helpers.request.call(this, options);
} catch (error) {
if (error.response && error.response.body && error.response.body.error) {
const message = error.response.body.error;
// Try to return the error prettier
throw new Error(
`Phantombuster error response [${error.statusCode}]: ${message}`,
);
}
throw error;
}
}
export function validateJSON(json: string | undefined, name: string): any { // tslint:disable-line:no-any
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
throw new Error(`${name} must provide a valid JSON`);
}
return result;
}

View file

@ -0,0 +1,274 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
phantombusterApiRequest,
validateJSON,
} from './GenericFunctions';
import {
agentFields,
agentOperations,
} from './AgentDescription';
// import {
// sentenceCase,
// } from 'change-case';
export class Phantombuster implements INodeType {
description: INodeTypeDescription = {
displayName: 'Phantombuster',
name: 'phantombuster',
icon: 'file:phantombuster.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Phantombuster API.',
defaults: {
name: 'Phantombuster',
color: '#62bfd7',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'phantombusterApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Agent',
value: 'agent',
},
],
default: 'agent',
description: 'The resource to operate on.',
},
...agentOperations,
...agentFields,
],
};
methods = {
loadOptions: {
async getAgents(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const responseData = await phantombusterApiRequest.call(
this,
'GET',
'/agents/fetch-all',
);
for (const item of responseData) {
console.log();
console.log(item.id);
console.log(item.name);
returnData.push({
name: item.name,
value: item.id,
});
}
return returnData;
},
// Get all the arguments to display them to user so that he can
// select them easily
// async getArguments(
// this: ILoadOptionsFunctions,
// ): Promise<INodePropertyOptions[]> {
// const returnData: INodePropertyOptions[] = [];
// const agentId = this.getCurrentNodeParameter('agentId') as string;
// const { argument } = await phantombusterApiRequest.call(
// this,
// 'GET',
// '/agents/fetch',
// {},
// { id: agentId },
// );
// for (const key of Object.keys(JSON.parse(argument))) {
// returnData.push({
// name: sentenceCase(key),
// value: key,
// });
// }
// return returnData;
// },
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = (items.length as unknown) as number;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'agent') {
//https://hub.phantombuster.com/reference#post_agents-delete-1
if (operation === 'delete') {
const agentId = this.getNodeParameter('agentId', i) as string;
responseData = await phantombusterApiRequest.call(
this,
'POST',
'/agents/delete',
{ id: agentId },
);
responseData = { success: true };
}
//https://hub.phantombuster.com/reference#get_agents-fetch-1
if (operation === 'get') {
const agentId = this.getNodeParameter('agentId', i) as string;
responseData = await phantombusterApiRequest.call(
this,
'GET',
'/agents/fetch',
{},
{ id: agentId },
);
}
//https://hub.phantombuster.com/reference#get_agents-fetch-output-1
if (operation === 'getOutput') {
const agentId = this.getNodeParameter('agentId', i) as string;
const resolveData = this.getNodeParameter('resolveData', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
Object.assign(qs, additionalFields);
qs.id = agentId;
responseData = await phantombusterApiRequest.call(
this,
'GET',
'/agents/fetch-output',
{},
qs,
);
if (resolveData === true) {
const { resultObject } = await phantombusterApiRequest.call(
this,
'GET',
'/containers/fetch-result-object',
{},
{ id: responseData.containerId },
);
if (resultObject === null) {
responseData = {};
} else {
responseData = JSON.parse(resultObject);
}
}
}
//https://api.phantombuster.com/api/v2/agents/fetch-all
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
responseData = await phantombusterApiRequest.call(
this,
'GET',
'/agents/fetch-all',
);
if (returnAll === false) {
const limit = this.getNodeParameter('limit', 0) as number;
responseData = responseData.splice(0, limit);
}
}
//https://hub.phantombuster.com/reference#post_agents-launch-1
if (operation === 'launch') {
const agentId = this.getNodeParameter('agentId', i) as string;
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
const resolveData = this.getNodeParameter('resolveData', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
id: agentId,
};
if (jsonParameters) {
if (additionalFields.argumentsJson) {
body.arguments = validateJSON(additionalFields.argumentsJson as string, 'Arguments');
delete additionalFields.argumentsJson;
}
if (additionalFields.bonusArgumentJson) {
body.bonusArgument = validateJSON(additionalFields.bonusArgumentJson as string, 'Bonus Argument');
delete additionalFields.bonusArgumentJson;
}
} else {
const argumentParameters = ((additionalFields.argumentsUi as IDataObject || {}).argumentValues as IDataObject[]) || [];
body.arguments = argumentParameters.reduce((object, currentValue) => {
object[currentValue.key as string] = currentValue.value;
return object;
}, {});
delete additionalFields.argumentsUi;
const bonusParameters = ((additionalFields.bonusArgumentUi as IDataObject || {}).bonusArgumentValue as IDataObject[]) || [];
body.bonusArgument = bonusParameters.reduce((object, currentValue) => {
object[currentValue.key as string] = currentValue.value;
return object;
}, {});
delete additionalFields.bonusArgumentUi;
}
Object.assign(body, additionalFields);
responseData = await phantombusterApiRequest.call(
this,
'POST',
'/agents/launch',
body,
);
if (resolveData === true) {
responseData = await phantombusterApiRequest.call(
this,
'GET',
'/containers/fetch',
{},
{ id: responseData.containerId },
);
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else if (responseData !== undefined) {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

View file

@ -161,6 +161,7 @@
"dist/credentials/PagerDutyApi.credentials.js",
"dist/credentials/PagerDutyOAuth2Api.credentials.js",
"dist/credentials/PayPalApi.credentials.js",
"dist/credentials/PhantombusterApi.credentials.js",
"dist/credentials/PipedriveApi.credentials.js",
"dist/credentials/PipedriveOAuth2Api.credentials.js",
"dist/credentials/PhilipsHueOAuth2Api.credentials.js",
@ -396,6 +397,7 @@
"dist/nodes/PagerDuty/PagerDuty.node.js",
"dist/nodes/PayPal/PayPal.node.js",
"dist/nodes/PayPal/PayPalTrigger.node.js",
"dist/nodes/Phantombuster/Phantombuster.node.js",
"dist/nodes/Pipedrive/Pipedrive.node.js",
"dist/nodes/Pipedrive/PipedriveTrigger.node.js",
"dist/nodes/PhilipsHue/PhilipsHue.node.js",