From f900bfe89762a28a5b8b21d1e1fbeb111e19bd10 Mon Sep 17 00:00:00 2001 From: Lorena Ciutacu <38855851+lorenanda@users.noreply.github.com> Date: Sun, 1 Aug 2021 13:27:57 +0200 Subject: [PATCH] :sparkles: Add Google Perspective node (#1807) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ add google perspective node * :sparkles: add language option * :zap: fix lint issues * :zap: Cleanup * :fire: Remove logging * :zap: Type all languages * :zap: Improvements Co-authored-by: ricardo Co-authored-by: Iván Ovejero --- .../GooglePerspectiveOAuth2Api.credentials.ts | 25 ++ .../Google/Perspective/GenericFunctions.ts | 40 +++ .../Perspective/GooglePerspective.node.json | 20 ++ .../Perspective/GooglePerspective.node.ts | 292 ++++++++++++++++++ .../nodes/Google/Perspective/perspective.svg | 30 ++ .../nodes/Google/Perspective/types.d.ts | 26 ++ packages/nodes-base/package.json | 2 + 7 files changed, 435 insertions(+) create mode 100644 packages/nodes-base/credentials/GooglePerspectiveOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Google/Perspective/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.json create mode 100644 packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.ts create mode 100644 packages/nodes-base/nodes/Google/Perspective/perspective.svg create mode 100644 packages/nodes-base/nodes/Google/Perspective/types.d.ts diff --git a/packages/nodes-base/credentials/GooglePerspectiveOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GooglePerspectiveOAuth2Api.credentials.ts new file mode 100644 index 0000000000..b3cd378767 --- /dev/null +++ b/packages/nodes-base/credentials/GooglePerspectiveOAuth2Api.credentials.ts @@ -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(' '), + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/Perspective/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Perspective/GenericFunctions.ts new file mode 100644 index 0000000000..ad93b1a0ec --- /dev/null +++ b/packages/nodes-base/nodes/Google/Perspective/GenericFunctions.ts @@ -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); + } +} diff --git a/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.json b/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.json new file mode 100644 index 0000000000..b785a9d94a --- /dev/null +++ b/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.json @@ -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/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.ts b/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.ts new file mode 100644 index 0000000000..662573de01 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.ts @@ -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 here', + 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 { + 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 { + 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((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)]; + } +} diff --git a/packages/nodes-base/nodes/Google/Perspective/perspective.svg b/packages/nodes-base/nodes/Google/Perspective/perspective.svg new file mode 100644 index 0000000000..2cfbaf8a3d --- /dev/null +++ b/packages/nodes-base/nodes/Google/Perspective/perspective.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/Google/Perspective/types.d.ts b/packages/nodes-base/nodes/Google/Perspective/types.d.ts new file mode 100644 index 0000000000..bb4ade830b --- /dev/null +++ b/packages/nodes-base/nodes/Google/Perspective/types.d.ts @@ -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; +}; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index f2b9649991..1448760dce 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -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",