diff --git a/packages/nodes-base/credentials/GooglePerspectiveOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GooglePerspectiveOAuth2Api.credentials.ts
new file mode 100644
index 000000000..b3cd37876
--- /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 000000000..ad93b1a0e
--- /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 000000000..b785a9d94
--- /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 000000000..662573de0
--- /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 000000000..2cfbaf8a3
--- /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 000000000..bb4ade830
--- /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 f2b964999..1448760dc 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",