Add Google Perspective node (#1807)

*  add google perspective node

*  add language option

*  fix lint issues

*  Cleanup

* 🔥 Remove logging

*  Type all languages

*  Improvements

Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
Lorena Ciutacu
2021-08-01 13:27:57 +02:00
committed by GitHub
parent ea29bc604d
commit f900bfe897
7 changed files with 435 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'https://www.googleapis.com/auth/userinfo.email',
];
export class GooglePerspectiveOAuth2Api implements ICredentialType {
name = 'googlePerspectiveOAuth2Api';
extends = [
'googleOAuth2Api',
];
displayName = 'Google Perspective OAuth2 API';
documentationUrl = 'google';
properties = [
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(' '),
},
];
}

View File

@@ -0,0 +1,40 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
NodeApiError,
} from 'n8n-workflow';
export async function googleApiRequest(
this: IExecuteFunctions,
method: 'POST',
endpoint: string,
body: IDataObject = {},
) {
const options: OptionsWithUri = {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
method,
body,
uri: `https://commentanalyzer.googleapis.com${endpoint}`,
json: true,
};
if (!Object.keys(body).length) {
delete options.body;
}
try {
return await this.helpers.requestOAuth2.call(this, 'googlePerspectiveOAuth2Api', options);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}

View File

@@ -0,0 +1,20 @@
{
"node": "n8n-nodes-base.perspective",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Development"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/perspective"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.perspective/"
}
]
}
}

View File

@@ -0,0 +1,292 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {
AttributesValuesUi,
CommentAnalyzeBody,
Language,
RequestedAttributes,
} from './types';
import {
googleApiRequest,
} from './GenericFunctions';
const ISO6391 = require('iso-639-1');
export class GooglePerspective implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Perspective',
name: 'googlePerspective',
icon: 'file:perspective.svg',
group: [
'transform',
],
version: 1,
description: 'Consume Google Perspective API',
subtitle: '={{$parameter["operation"]}}',
defaults: {
name: 'Google Perspective',
color: '#200647',
},
inputs: [
'main',
],
outputs: [
'main',
],
credentials: [
{
name: 'googlePerspectiveOAuth2Api',
required: true,
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Analyze Comment',
value: 'analyzeComment',
},
],
default: 'analyzeComment',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'analyzeComment',
],
},
},
},
{
displayName: 'Attributes to Analyze',
name: 'requestedAttributesUi',
type: 'fixedCollection',
default: '',
typeOptions: {
multipleValues: true,
},
placeholder: 'Add Atrribute',
required: true,
displayOptions: {
show: {
operation: [
'analyzeComment',
],
},
},
options: [
{
displayName: 'Properties',
name: 'requestedAttributesValues',
values: [
{
displayName: 'Attribute Name',
name: 'attributeName',
type: 'options',
options: [
{
name: 'Flirtation',
value: 'flirtation',
},
{
name: 'Identity Attack',
value: 'identity_attack',
},
{
name: 'Insult',
value: 'insult',
},
{
name: 'Profanity',
value: 'profanity',
},
{
name: 'Severe Toxicity',
value: 'severe_toxicity',
},
{
name: 'Sexually Explicit',
value: 'sexually_explicit',
},
{
name: 'Threat',
value: 'threat',
},
{
name: 'Toxicity',
value: 'toxicity',
},
],
description: 'Attribute to analyze in the text. Details <a target="_blank" href="https://developers.perspectiveapi.com/s/about-the-api-attributes-and-languages">here</a>',
default: 'flirtation',
},
{
displayName: 'Score Threshold',
name: 'scoreThreshold',
type: 'number',
typeOptions: {
numberStepSize: 0.1,
numberPrecision: 2,
minValue: 0,
maxValue: 1,
},
description: 'Score above which to return results. At zero, all scores are returned.',
default: 0,
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: [
'analyzeComment',
],
},
},
default: {},
placeholder: 'Add Option',
options: [
{
displayName: 'Languages',
name: 'languages',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLanguages',
},
default: '',
description: 'Languages of the text input. If unspecified, the API will auto-detect the comment language',
},
],
},
],
};
methods = {
loadOptions: {
// Get all the available languages to display them to user so that he can
// select them easily
async getLanguages(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const supportedLanguages = [
'English',
'Spanish',
'French',
'German',
'Portuguese',
'Italian',
'Russian',
];
const languages = ISO6391.getAllNames().filter((language: string) => supportedLanguages.includes(language));
for (const language of languages) {
const languageName = language;
const languageId = ISO6391.getCode(language);
returnData.push({
name: languageName,
value: languageId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0);
const returnData: IDataObject[] = [];
let responseData;
for (let i = 0; i < items.length; i++) {
try {
if (operation === 'analyzeComment') {
// https://developers.perspectiveapi.com/s/about-the-api-methods
const attributes = this.getNodeParameter(
'requestedAttributesUi.requestedAttributesValues', i, [],
) as AttributesValuesUi[];
if (!attributes.length) {
throw new NodeOperationError(
this.getNode(),
'Please enter at least one attribute to analyze.',
);
}
const requestedAttributes = attributes.reduce<RequestedAttributes>((acc, cur) => {
return Object.assign(acc, {
[cur.attributeName.toUpperCase()]: {
scoreType: 'probability',
scoreThreshold: cur.scoreThreshold,
},
});
}, {});
const body: CommentAnalyzeBody = {
comment: {
type: 'PLAIN_TEXT',
text: this.getNodeParameter('text', i) as string,
},
requestedAttributes,
};
const { languages } = this.getNodeParameter('options', i) as { languages: Language };
if (languages?.length) {
body.languages = languages;
}
responseData = await googleApiRequest.call(this, 'POST', '/v1alpha1/comments:analyze', body);
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.message });
continue;
}
throw error;
}
Array.isArray(responseData)
? returnData.push(...responseData)
: returnData.push(responseData);
}
return [this.helpers.returnJsonArray(responseData)];
}
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="6 10 125 125"
version="1.1"
id="svg4"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath937">
<rect
style="fill:#0000ff;fill-rule:evenodd"
id="rect939"
width="123.4947"
height="119.02773"
x="12.25"
y="12.482271" />
</clipPath>
</defs>
<g
id="g532"
clip-path="url(#clipPath937)">
<path
d="M131.53 72a59.79 59.79 0 0 1-53.81 59.49v-119A59.79 59.79 0 0 1 131.53 72zM12.25 78v53.51h53.52A59.81 59.81 0 0 0 12.25 78zm0-12h53.52V12.51A59.81 59.81 0 0 0 12.25 66zm176.83-37.06H223c21.77 0 35.8 11.32 35.8 29.65s-14 29.64-35.8 29.64h-15.22v26.82h-18.7zm33.34 44c11.81 0 17.34-4.55 17.34-14.39s-5.53-14.4-17.34-14.4h-14.64V73zm37.14 11.72v-.61c0-19.93 12.91-33 32-33 18 0 31 12.67 31 33.46v4.18h-44.9c1 9.72 6 14.27 14.14 14.27 5.91 0 10.09-2.58 11.94-7h18.2c-2.7 12.42-14.64 21-30.26 21-19.82.04-32.12-11.96-32.12-32.3zm18.57-7.87h26.2c-1.72-8.12-6.27-12.18-12.91-12.18s-11.69 3.94-13.29 12.18zm54.73-23.74h17.59v9.47C354.26 55.88 361 52.31 370 52.31h3.56v17h-15.5c-4.67 0-7.13 2.46-7.13 7v38.75h-18.07zM378.12 96H395c.62 4.92 4.55 7.5 13.29 7.5 7.38 0 10.94-2.33 10.94-5.65 0-2.71-1.72-3.94-6.15-4.8l-14.79-2.85c-13.65-2.46-19.06-8.12-19.06-18.08 0-12.3 10-21 27.92-21 16.73 0 28.17 8.37 29 20.54h-16.82c-.74-4.42-4.8-7-12.3-7-7 0-10.46 2.22-10.46 5.54 0 2.58 1.72 3.81 6 4.67l14.88 2.83c13.53 2.46 18.94 8.12 18.94 18.21 0 12.91-10.45 21.15-28.29 21.15-18.79-.06-29.37-8.78-29.98-21.06zm68.13-42.95h17.59v9.84c3.7-7.38 10.71-11.56 19.56-11.56 14.89 0 26.33 11.68 26.33 32v.62c0 20.17-12.06 32.84-26.94 32.84-8.12 0-14.76-3.32-18.45-9.47v30.63h-18.09zM491 84.42v-1.6c0-11.32-4.92-17.35-13.16-17.35-8.61 0-13.53 5.91-13.53 17.23v2.7c0 12.18 5 17.22 13.16 17.22 7.9 0 13.53-6.62 13.53-18.2zm26.1.24v-.61c0-19.93 12.92-33 32-33 18 0 31 12.67 31 33.46v4.18h-44.9c1 9.72 6 14.27 14.15 14.27 5.9 0 10.08-2.58 11.93-7h18.2c-2.7 12.42-14.63 21-30.25 21-19.83.04-32.13-11.96-32.13-32.3zm18.57-7.87h26.21c-1.73-8.12-6.28-12.18-12.92-12.18s-11.69 3.94-13.29 12.18zm51.53 7.75v-.74c0-19.31 13.16-32.72 31.37-32.72 16.24 0 27.8 9.35 29.4 24.6h-17.71c-.74-6.64-5.42-10.45-11.44-10.45-7.88 0-13 6.52-13 18.2v1.48c0 11.81 5.16 18 12.79 18 6.52 0 10.95-4.06 11.81-11.2h17.71c-1.6 15.75-13 25.34-29.77 25.34C600 117 587.2 104.34 587.2 84.54zm76.01 9.35V67.32h-10.7V53.05h10.7v-18h18v18h18.21v14.27h-18.25v30.75c0 1.72.74 2.46 2.46 2.46h15.75v14.52H685.6c-14.52 0-22.39-7.51-22.39-21.16zm46-58.31a10.09 10.09 0 0 1 20.17 0 10.09 10.09 0 1 1-20.17 0zm1 17.47h18.08v62h-18.1zm25.19 0H754l14.39 45.27 14.27-45.27h17.71l-20.91 62h-23.15zm68.88 31.61v-.61c0-19.93 12.92-33 32-33 18 0 31 12.67 31 33.46v4.18h-44.9c1 9.72 6 14.27 14.15 14.27 5.9 0 10.09-2.58 11.93-7h18.21c-2.71 12.42-14.64 21-30.26 21-19.83.04-32.13-11.96-32.13-32.3zm18.57-7.87h26.21c-1.73-8.12-6.28-12.18-12.92-12.18s-11.69 3.94-13.29 12.18z"
fill="#451735"
id="path2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,26 @@
export type CommentAnalyzeBody = {
comment: Comment;
requestedAttributes: RequestedAttributes;
languages?: Language;
};
export type Language = 'de' | 'en' | 'fr' | 'ar' | 'es' | 'it' | 'pt' | 'ru';
export type Comment = {
text?: string;
type?: string;
};
export type RequestedAttributes = {
[key: string]: {
scoreType?: string;
scoreThreshold?: {
value: number
};
};
};
export type AttributesValuesUi = {
attributeName: string;
scoreThreshold: number;
};

View File

@@ -111,6 +111,7 @@
"dist/credentials/GoogleFirebaseCloudFirestoreOAuth2Api.credentials.js",
"dist/credentials/GoogleFirebaseRealtimeDatabaseOAuth2Api.credentials.js",
"dist/credentials/GoogleOAuth2Api.credentials.js",
"dist/credentials/GooglePerspectiveOAuth2Api.credentials.js",
"dist/credentials/GoogleSheetsOAuth2Api.credentials.js",
"dist/credentials/GoogleSlidesOAuth2Api.credentials.js",
"dist/credentials/GSuiteAdminOAuth2Api.credentials.js",
@@ -401,6 +402,7 @@
"dist/nodes/Google/Firebase/RealtimeDatabase/RealtimeDatabase.node.js",
"dist/nodes/Google/Gmail/Gmail.node.js",
"dist/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.js",
"dist/nodes/Google/Perspective/GooglePerspective.node.js",
"dist/nodes/Google/Sheet/GoogleSheets.node.js",
"dist/nodes/Google/Slides/GoogleSlides.node.js",
"dist/nodes/Google/Task/GoogleTasks.node.js",