diff --git a/packages/cli/package.json b/packages/cli/package.json index 5ab8153064..0b4e2fa9cb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -103,6 +103,7 @@ "n8n-editor-ui": "~0.45.0", "n8n-nodes-base": "~0.62.1", "n8n-workflow": "~0.31.0", + "oauth-1.0a": "^2.2.6", "open": "^7.0.0", "pg": "^7.11.0", "request-promise-native": "^1.0.7", diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index d9c760a7c9..88e60d4c52 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -13,10 +13,13 @@ import { import * as bodyParser from 'body-parser'; require('body-parser-xml')(bodyParser); import * as history from 'connect-history-api-fallback'; -import * as requestPromise from 'request-promise-native'; import * as _ from 'lodash'; import * as clientOAuth2 from 'client-oauth2'; +import * as clientOAuth1 from 'oauth-1.0a'; +import { RequestOptions } from 'oauth-1.0a'; import * as csrf from 'csrf'; +import * as requestPromise from 'request-promise-native'; +import { createHmac } from 'crypto'; import { ActiveExecutions, @@ -90,7 +93,8 @@ import * as jwks from 'jwks-rsa'; // @ts-ignore import * as timezones from 'google-timezones-json'; import * as parseUrl from 'parseurl'; - +import * as querystring from 'querystring'; +import { OptionsWithUrl } from 'request-promise-native'; class App { @@ -890,6 +894,158 @@ class App { return returnData; })); + // ---------------------------------------- + // OAuth1-Credential/Auth + // ---------------------------------------- + + // Authorize OAuth Data + this.app.get('/rest/oauth1-credential/auth', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + if (req.query.id === undefined) { + throw new Error('Required credential id is missing!'); + } + + const result = await Db.collections.Credentials!.findOne(req.query.id as string); + if (result === undefined) { + res.status(404).send('The credential is not known.'); + return ''; + } + + let encryptionKey = undefined; + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + throw new Error('No encryption key got found to decrypt the credentials!'); + } + + // Decrypt the currently saved credentials + const workflowCredentials: IWorkflowCredentials = { + [result.type as string]: { + [result.name as string]: result as ICredentialsEncrypted, + }, + }; + const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey); + const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type); + + const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string; + + const oauth = new clientOAuth1({ + consumer: { + key: _.get(oauthCredentials, 'consumerKey') as string, + secret: _.get(oauthCredentials, 'consumerSecret') as string, + }, + signature_method: signatureMethod, + hash_function(base, key) { + const algorithm = (signatureMethod === 'HMAC-SHA1') ? 'sha1' : 'sha256'; + return createHmac(algorithm, key) + .update(base) + .digest('base64'); + }, + }); + + const callback = `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth1-credential/callback?cid=${req.query.id}`; + + const options: RequestOptions = { + method: 'POST', + url: (_.get(oauthCredentials, 'requestTokenUrl') as string), + data: { + oauth_callback: callback, + }, + }; + + const data = oauth.toHeader(oauth.authorize(options as RequestOptions)); + + //@ts-ignore + options.headers = data; + + const response = await requestPromise(options); + + // Response comes as x-www-form-urlencoded string so convert it to JSON + + const responseJson = querystring.parse(response); + + const returnUri = `${_.get(oauthCredentials, 'authUrl')}?oauth_token=${responseJson.oauth_token}`; + + // Encrypt the data + const credentials = new Credentials(result.name, result.type, result.nodesAccess); + + credentials.setData(decryptedDataOriginal, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); + + // Update the credentials in DB + await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData); + + return returnUri; + })); + + // Verify and store app code. Generate access tokens and store for respective credential. + this.app.get('/rest/oauth1-credential/callback', async (req: express.Request, res: express.Response) => { + const { oauth_verifier, oauth_token, cid } = req.query; + + if (oauth_verifier === undefined || oauth_token === undefined) { + throw new Error('Insufficient parameters for OAuth1 callback'); + } + + const result = await Db.collections.Credentials!.findOne(cid as any); + if (result === undefined) { + const errorResponse = new ResponseHelper.ResponseError('The credential is not known.', undefined, 404); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + let encryptionKey = undefined; + encryptionKey = await UserSettings.getEncryptionKey(); + if (encryptionKey === undefined) { + const errorResponse = new ResponseHelper.ResponseError('No encryption key got found to decrypt the credentials!', undefined, 503); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + // Decrypt the currently saved credentials + const workflowCredentials: IWorkflowCredentials = { + [result.type as string]: { + [result.name as string]: result as ICredentialsEncrypted, + }, + }; + const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey); + const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type); + + const options: OptionsWithUrl = { + method: 'POST', + url: _.get(oauthCredentials, 'accessTokenUrl') as string, + qs: { + oauth_token, + oauth_verifier, + } + }; + + let oauthToken; + + try { + oauthToken = await requestPromise(options); + } catch (error) { + const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); + return ResponseHelper.sendErrorResponse(res, errorResponse); + } + + // Response comes as x-www-form-urlencoded string so convert it to JSON + + const oauthTokenJson = querystring.parse(oauthToken); + + decryptedDataOriginal.oauthTokenData = oauthTokenJson; + + const credentials = new Credentials(result.name, result.type, result.nodesAccess); + credentials.setData(decryptedDataOriginal, encryptionKey); + const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; + // Add special database related data + newCredentialsData.updatedAt = this.getCurrentDate(); + // Save the credentials in DB + await Db.collections.Credentials!.update(cid as any, newCredentialsData); + + res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html')); + }); + // ---------------------------------------- // OAuth2-Credential/Auth diff --git a/packages/core/src/Interfaces.ts b/packages/core/src/Interfaces.ts index 4b0a5ed451..5aa0e10a64 100644 --- a/packages/core/src/Interfaces.ts +++ b/packages/core/src/Interfaces.ts @@ -18,7 +18,7 @@ import { } from 'n8n-workflow'; -import { OptionsWithUri } from 'request'; +import { OptionsWithUri, OptionsWithUrl } from 'request'; import * as requestPromise from 'request-promise-native'; interface Constructable { @@ -36,7 +36,8 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -46,7 +47,8 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -55,7 +57,8 @@ export interface IPollFunctions extends IPollFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -70,7 +73,8 @@ export interface ITriggerFunctions extends ITriggerFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } @@ -94,7 +98,8 @@ export interface IUserSettings { export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { helpers: { request?: requestPromise.RequestPromiseAPI, - requestOAuth?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions) => Promise, // tslint:disable-line:no-any + requestOAuth2?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions) => Promise, // tslint:disable-line:no-any + requestOAuth1?(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -102,7 +107,8 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { export interface IHookFunctions extends IHookFunctionsBase { helpers: { request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -111,7 +117,8 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index f11abba582..f0d0d01751 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -36,15 +36,20 @@ import { WorkflowExecuteMode, } from 'n8n-workflow'; +import * as clientOAuth1 from 'oauth-1.0a'; +import { RequestOptions, Token } from 'oauth-1.0a'; import * as clientOAuth2 from 'client-oauth2'; import { get } from 'lodash'; import * as express from 'express'; import * as path from 'path'; -import { OptionsWithUri } from 'request'; +import { OptionsWithUrl, OptionsWithUri } from 'request'; import * as requestPromise from 'request-promise-native'; import { Magic, MAGIC_MIME_TYPE } from 'mmmagic'; +import { createHmac } from 'crypto'; + + const magic = new Magic(MAGIC_MIME_TYPE); @@ -116,7 +121,7 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m * @param {IWorkflowExecuteAdditionalData} additionalData * @returns */ -export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string, property?: string) { +export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string, property?: string) { const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; if (credentials === undefined) { @@ -170,6 +175,63 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string }); } +/* Makes a request using OAuth1 data for authentication +* +* @export +* @param {IAllExecuteFunctions} this +* @param {string} credentialsType +* @param {(OptionsWithUrl | requestPromise.RequestPromiseOptions)} requestOptionså +* @returns +*/ +export function requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions) { + const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + if (credentials.oauthTokenData === undefined) { + throw new Error('OAuth credentials not connected!'); + } + + const oauth = new clientOAuth1({ + consumer: { + key: credentials.consumerKey as string, + secret: credentials.consumerSecret as string, + }, + signature_method: credentials.signatureMethod as string, + hash_function(base, key) { + const algorithm = (credentials.signatureMethod === 'HMAC-SHA1') ? 'sha1' : 'sha256'; + return createHmac(algorithm, key) + .update(base) + .digest('base64'); + }, + }); + + const oauthTokenData = credentials.oauthTokenData as IDataObject; + + const token: Token = { + key: oauthTokenData.oauth_token as string, + secret: oauthTokenData.oauth_token_secret as string, + }; + + const newRequestOptions = { + //@ts-ignore + url: requestOptions.url, + method: requestOptions.method, + data: requestOptions.body, + json: requestOptions.json, + }; + + //@ts-ignore + newRequestOptions.form = oauth.authorize(newRequestOptions as RequestOptions, token); + + return this.helpers.request!(newRequestOptions) + .catch(async (error: IResponseError) => { + // Unknown error so simply throw it + throw error; + }); +} /** @@ -462,8 +524,11 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, }, @@ -522,8 +587,11 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, }, @@ -615,8 +683,11 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, }, @@ -710,8 +781,11 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, }, }; @@ -763,8 +837,11 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, }, }; @@ -827,8 +904,11 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, }, }; @@ -918,8 +998,11 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + }, + requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any + return requestOAuth1.call(this, credentialsType, requestOptions); }, returnJsonArray, }, diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index e0e6355359..63ea4223a9 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -145,6 +145,7 @@ export interface IRestApi { deleteExecutions(sendData: IExecutionDeleteFilter): Promise; retryExecution(id: string, loadWorkflow?: boolean): Promise; getTimezones(): Promise; + oAuth1CredentialAuthorize(sendData: ICredentialsResponse): Promise; oAuth2CredentialAuthorize(sendData: ICredentialsResponse): Promise; oAuth2Callback(code: string, state: string): Promise; } diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index db9f643d52..ba8121ddbe 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -47,13 +47,13 @@ Not all required credential properties are filled - + Is connected - + Is NOT connected @@ -222,8 +222,11 @@ export default mixins( if (this.credentialTypeData.name === 'oAuth2Api') { return true; } + if (this.credentialTypeData.name === 'oAuth1Api') { + return true; + } const types = this.parentTypes(this.credentialTypeData.name); - return types.includes('oAuth2Api'); + return types.includes('oAuth2Api') || types.includes('oAuth1Api'); }, isOAuthConnected (): boolean { if (this.isOAuthType === false) { @@ -233,7 +236,9 @@ export default mixins( return this.credentialDataDynamic !== null && !!this.credentialDataDynamic.data!.oauthTokenData; }, oAuthCallbackUrl (): string { - return this.$store.getters.getWebhookBaseUrl + 'rest/oauth2-credential/callback'; + const types = this.parentTypes(this.credentialTypeData.name); + const oauthType = (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) ? 'oauth2' : 'oauth1'; + return this.$store.getters.getWebhookBaseUrl + `rest/${oauthType}-credential/callback`; }, requiredPropertiesFilled (): boolean { for (const property of this.credentialProperties) { @@ -323,7 +328,7 @@ export default mixins( return result; }, - async oAuth2CredentialAuthorize () { + async oAuthCredentialAuthorize () { let url; let credentialData = this.credentialDataDynamic; @@ -350,8 +355,14 @@ export default mixins( } } + const types = this.parentTypes(this.credentialTypeData.name); + try { - url = await this.restApi().oAuth2CredentialAuthorize(credentialData as ICredentialsResponse) as string; + if (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) { + url = await this.restApi().oAuth2CredentialAuthorize(credentialData as ICredentialsResponse) as string; + } else if (this.credentialTypeData.name === 'oAuth1Api' || types.includes('oAuth1Api')) { + url = await this.restApi().oAuth1CredentialAuthorize(credentialData as ICredentialsResponse) as string; + } } catch (error) { this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:'); return; diff --git a/packages/editor-ui/src/components/mixins/restApi.ts b/packages/editor-ui/src/components/mixins/restApi.ts index be114786a9..bca3fff338 100644 --- a/packages/editor-ui/src/components/mixins/restApi.ts +++ b/packages/editor-ui/src/components/mixins/restApi.ts @@ -252,6 +252,11 @@ export const restApi = Vue.extend({ return self.restApi().makeRestApiRequest('GET', `/credential-types`); }, + // Get OAuth1 Authorization URL using the stored credentials + oAuth1CredentialAuthorize: (sendData: ICredentialsResponse): Promise => { + return self.restApi().makeRestApiRequest('GET', `/oauth1-credential/auth`, sendData); + }, + // Get OAuth2 Authorization URL using the stored credentials oAuth2CredentialAuthorize: (sendData: ICredentialsResponse): Promise => { return self.restApi().makeRestApiRequest('GET', `/oauth2-credential/auth`, sendData); diff --git a/packages/nodes-base/credentials/OAuth1Api.credentials.ts b/packages/nodes-base/credentials/OAuth1Api.credentials.ts new file mode 100644 index 0000000000..d5c18445f0 --- /dev/null +++ b/packages/nodes-base/credentials/OAuth1Api.credentials.ts @@ -0,0 +1,63 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class OAuth1Api implements ICredentialType { + name = 'oAuth1Api'; + displayName = 'OAuth1 API'; + properties = [ + { + displayName: 'Consumer Key', + name: 'consumerKey', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Consumer Secret', + name: 'consumerSecret', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Request Token URL', + name: 'requestTokenUrl', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Signature Method', + name: 'signatureMethod', + type: 'options' as NodePropertyTypes, + options: [ + { + name: 'HMAC-SHA1', + value: 'HMAC-SHA1' + }, + { + name: 'HMAC-SHA256', + value: 'HMAC-SHA256' + }, + ], + default: '', + required: true, + }, + ]; +} diff --git a/packages/nodes-base/credentials/TwitterOAuth1Api.credentials.ts b/packages/nodes-base/credentials/TwitterOAuth1Api.credentials.ts new file mode 100644 index 0000000000..60c1d70064 --- /dev/null +++ b/packages/nodes-base/credentials/TwitterOAuth1Api.credentials.ts @@ -0,0 +1,38 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TwitterOAuth1Api implements ICredentialType { + name = 'twitterOAuth1Api'; + extends = [ + 'oAuth1Api', + ]; + displayName = 'Twitter OAuth API'; + properties = [ + { + displayName: 'Request Token URL', + name: 'requestTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.twitter.com/oauth/request_token', + }, + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.twitter.com/oauth/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.twitter.com/oauth/access_token', + }, + { + displayName: 'Signature Method', + name: 'signatureMethod', + type: 'hidden' as NodePropertyTypes, + default: 'HMAC-SHA1', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Github/GenericFunctions.ts b/packages/nodes-base/nodes/Github/GenericFunctions.ts index f6c5d076c6..592367019e 100644 --- a/packages/nodes-base/nodes/Github/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Github/GenericFunctions.ts @@ -51,7 +51,7 @@ export async function githubApiRequest(this: IHookFunctions | IExecuteFunctions, const baseUrl = credentials!.server || 'https://api.github.com'; options.uri = `${baseUrl}${endpoint}`; - return await this.helpers.requestOAuth.call(this, 'githubOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'githubOAuth2Api', options); } } catch (error) { if (error.statusCode === 401) { diff --git a/packages/nodes-base/nodes/Google/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GenericFunctions.ts index 80edb44370..b8c18429ef 100644 --- a/packages/nodes-base/nodes/Google/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/GenericFunctions.ts @@ -27,7 +27,7 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF delete options.body; } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'googleOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'googleOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.message) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts index 76b64ed3e7..a43545a32e 100644 --- a/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts +++ b/packages/nodes-base/nodes/HelpScout/GenericFunctions.ts @@ -32,7 +32,7 @@ export async function helpscoutApiRequest(this: IExecuteFunctions | IExecuteSing delete options.body; } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'helpScoutOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'helpScoutOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body._embedded diff --git a/packages/nodes-base/nodes/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest.node.ts index 990c007e07..499575a728 100644 --- a/packages/nodes-base/nodes/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest.node.ts @@ -801,7 +801,7 @@ export class HttpRequest implements INodeType { // Now that the options are all set make the actual http request if (oAuth2Api !== undefined) { - response = await this.helpers.requestOAuth.call(this, 'oAuth2Api', requestOptions); + response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions); } else { response = await this.helpers.request(requestOptions); } diff --git a/packages/nodes-base/nodes/Keap/GenericFunctions.ts b/packages/nodes-base/nodes/Keap/GenericFunctions.ts index c04fb057d5..d00228fadc 100644 --- a/packages/nodes-base/nodes/Keap/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Keap/GenericFunctions.ts @@ -37,7 +37,7 @@ export async function keapApiRequest(this: IWebhookFunctions | IHookFunctions | delete options.body; } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'keapOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'keapOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.message) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts index 33090090b3..4180b4336b 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/GenericFunctions.ts @@ -24,7 +24,7 @@ export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSing options.headers = Object.assign({}, options.headers, headers); } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'microsoftExcelOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'microsoftExcelOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts index 1bd1bee202..7040bb053e 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts @@ -35,7 +35,7 @@ export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSing } //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'microsoftOneDriveOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'microsoftOneDriveOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/OAuth.node.ts b/packages/nodes-base/nodes/OAuth.node.ts index 4f5ff5debb..685b07cc40 100644 --- a/packages/nodes-base/nodes/OAuth.node.ts +++ b/packages/nodes-base/nodes/OAuth.node.ts @@ -138,7 +138,7 @@ export class OAuth implements INodeType { json: true, }; - const responseData = await this.helpers.requestOAuth.call(this, 'oAuth2Api', requestOptions); + const responseData = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions); return [this.helpers.returnJsonArray(responseData)]; } else { throw new Error('Unknown operation'); diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index c8cd97d9fa..648735feb2 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -20,7 +20,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin }; try { //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'salesforceOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'salesforceOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body[0] && error.response.body[0].message) { // Try to return the error prettier diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index 7d65e1a93a..ad04536b36 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -43,7 +43,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu return await this.helpers.request(options); } else { //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'slackOAuth2Api', options, 'bearer', 'authed_user.access_token'); + return await this.helpers.requestOAuth2.call(this, 'slackOAuth2Api', options, 'bearer', 'authed_user.access_token'); } } catch (error) { if (error.statusCode === 401) { diff --git a/packages/nodes-base/nodes/Twitter/GenericFunctions.ts b/packages/nodes-base/nodes/Twitter/GenericFunctions.ts new file mode 100644 index 0000000000..c47f63cb17 --- /dev/null +++ b/packages/nodes-base/nodes/Twitter/GenericFunctions.ts @@ -0,0 +1,44 @@ +import { + OptionsWithUrl, + } from 'request'; + +import { + IHookFunctions, + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function twitterApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + let options: OptionsWithUrl = { + method, + body, + qs, + url: uri || `https://api.twitter.com/1.1${resource}`, + json: true + }; + try { + if (Object.keys(option).length !== 0) { + options = Object.assign({}, options, option); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth1.call(this, 'twitterOAuth1Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.errors) { + // Try to return the error prettier + const errorMessages = error.response.body.errors.map((error: IDataObject) => { + return error.message; + }); + throw new Error(`Twitter error response [${error.statusCode}]: ${errorMessages.join(' | ')}`); + } + + throw error; + } +} diff --git a/packages/nodes-base/nodes/Twitter/TweetDescription.ts b/packages/nodes-base/nodes/Twitter/TweetDescription.ts new file mode 100644 index 0000000000..5b97406b40 --- /dev/null +++ b/packages/nodes-base/nodes/Twitter/TweetDescription.ts @@ -0,0 +1,208 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const tweetOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'tweet', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new tweet', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const tweetFields = [ +/* -------------------------------------------------------------------------- */ +/* tweet:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: true, + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'tweet', + ], + }, + }, + description: 'The text of the status update. URL encode as necessary. t.co link wrapping will affect character counts. ', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'tweet', + ], + }, + }, + options: [ + { + displayName: 'Attachments', + name: 'attachmentsUi', + placeholder: 'Add Attachments', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'attachmentsValues', + displayName: 'Attachments Values', + values: [ + { + displayName: 'Data', + name: 'data', + type: 'string', + default: '', + description: 'The base64-encoded file content being uploaded.', + }, + { + displayName: 'Category', + name: 'category', + type: 'options', + options: [ + { + name: 'Amplify Video', + value: 'amplifyVideo', + }, + { + name: 'Gif', + value: 'tweetGif', + }, + { + name: 'Image', + value: 'tweetImage', + }, + { + name: 'Video', + value: 'tweetVideo', + }, + ], + default: '', + description: 'The category that represents how the media will be used', + }, + ], + }, + { + name: 'attachmentsBinary', + displayName: 'Attachments Binary', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'string', + default: '', + description: 'Name of the binary properties which contain data which should be added to email as attachment', + }, + { + displayName: 'Category', + name: 'category', + type: 'options', + options: [ + { + name: 'Amplify Video', + value: 'amplifyVideo', + }, + { + name: 'Gif', + value: 'tweetGif', + }, + { + name: 'Image', + value: 'tweetImage', + }, + { + name: 'Video', + value: 'tweetVideo', + }, + ], + default: '', + description: 'The category that represents how the media will be used', + }, + ], + }, + ], + default: '', + description: 'Array of supported attachments to add to the message.', + }, + { + displayName: 'Display Coordinates', + name: 'displayCoordinates', + type: 'boolean', + default: false, + description: 'Whether or not to put a pin on the exact coordinates a Tweet has been sent from.', + }, + { + displayName: 'Location', + name: 'locationFieldsUi', + type: 'fixedCollection', + placeholder: 'Add Location', + default: {}, + description: `Subscriber location information.n`, + options: [ + { + name: 'locationFieldsValues', + displayName: 'Location', + values: [ + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + required: true, + description: 'The location latitude.', + default: '', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + required: true, + description: 'The location longitude.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Possibly Sensitive', + name: 'possiblySensitive', + type: 'boolean', + default: false, + description: 'If you upload Tweet media that might be considered sensitive content such as nudity, or medical procedures, you must set this value to true.', + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twitter/TweetInterface.ts b/packages/nodes-base/nodes/Twitter/TweetInterface.ts new file mode 100644 index 0000000000..10b16d6a2d --- /dev/null +++ b/packages/nodes-base/nodes/Twitter/TweetInterface.ts @@ -0,0 +1,8 @@ +export interface ITweet { + display_coordinates?: boolean; + lat?: number; + long?: number; + media_ids?: string; + possibly_sensitive?: boolean; + status: string; +} diff --git a/packages/nodes-base/nodes/Twitter/Twitter.node.ts b/packages/nodes-base/nodes/Twitter/Twitter.node.ts new file mode 100644 index 0000000000..6e17373674 --- /dev/null +++ b/packages/nodes-base/nodes/Twitter/Twitter.node.ts @@ -0,0 +1,151 @@ + +import { + IExecuteFunctions, + } from 'n8n-core'; + +import { + IBinaryKeyData, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + tweetFields, + tweetOperations, +} from './TweetDescription'; + +import { + twitterApiRequest, +} from './GenericFunctions'; + +import { + ITweet, + } from './TweetInterface'; + + import { + snakeCase, + } from 'change-case'; + +export class Twitter implements INodeType { + description: INodeTypeDescription = { + displayName: 'Twitter ', + name: 'twitter', + icon: 'file:twitter.png', + group: ['input', 'output'], + version: 1, + description: 'Consume Twitter API', + subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}', + defaults: { + name: 'Twitter', + color: '#1DA1F2', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'twitterOAuth1Api', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Tweet', + value: 'tweet', + }, + ], + default: 'tweet', + description: 'The resource to operate on.', + }, + // TWEET + ...tweetOperations, + ...tweetFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + 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 === 'tweet') { + //https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update + if (operation === 'create') { + const text = this.getNodeParameter('text', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: ITweet = { + status: text, + }; + if (additionalFields.attachmentsUi) { + const mediaIds = []; + const attachmentsUi = additionalFields.attachmentsUi as IDataObject; + const uploadUri = 'https://upload.twitter.com/1.1/media/upload.json'; + if (attachmentsUi.attachmentsValues) { + const attachtments = attachmentsUi.attachmentsValues as IDataObject[]; + for (const attachment of attachtments) { + const body = { + media_data: attachment.data, + media_category: snakeCase(attachment.category as string).toUpperCase(), + }; + const response = await twitterApiRequest.call(this, 'POST', '', body, {}, uploadUri); + mediaIds.push(response.media_id); + } + } + if (attachmentsUi.attachmentsBinary) { + const attachtments = attachmentsUi.attachmentsBinary as IDataObject[]; + for (const attachment of attachtments) { + + const binaryData = items[i].binary as IBinaryKeyData; + const propertyName = attachment.property as string; + + if (binaryData === undefined) { + throw new Error('No binary data set. So file can not be written!'); + } + + if (!binaryData[propertyName]) { + continue; + } + + const body = { + media_data: binaryData[propertyName].data, + media_category: snakeCase(attachment.category as string).toUpperCase(), + }; + const response = await twitterApiRequest.call(this, 'POST', '', body, {}, uploadUri); + mediaIds.push(response.media_id_string); + } + } + body.media_ids = mediaIds.join(','); + } + if (additionalFields.possiblySensitive) { + body.possibly_sensitive = additionalFields.possibly_sensitive as boolean; + } + if (additionalFields.locationFieldsUi) { + const locationUi = additionalFields.locationFieldsUi as IDataObject; + if (locationUi.locationFieldsValues) { + const values = locationUi.locationFieldsValues as IDataObject; + body.lat = parseFloat(values.lalatitude as string); + body.long = parseFloat(values.lalatitude as string); + } + } + responseData = await twitterApiRequest.call(this, 'POST', '/statuses/update.json', body); + } + } + 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)]; + } +} diff --git a/packages/nodes-base/nodes/Twitter/twitter.png b/packages/nodes-base/nodes/Twitter/twitter.png new file mode 100644 index 0000000000..6f4436e441 Binary files /dev/null and b/packages/nodes-base/nodes/Twitter/twitter.png differ diff --git a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts index 4deea5f6f5..13ebecd4c3 100644 --- a/packages/nodes-base/nodes/Zoho/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zoho/GenericFunctions.ts @@ -25,7 +25,7 @@ export async function zohoApiRequest(this: IExecuteFunctions | IExecuteSingleFun }; try { //@ts-ignore - return await this.helpers.requestOAuth.call(this, 'zohoOAuth2Api', options); + return await this.helpers.requestOAuth2.call(this, 'zohoOAuth2Api', options); } catch (error) { if (error.response && error.response.body && error.response.body.message) { // Try to return the error prettier diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6ffe5a1e28..43782e74a2 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -91,6 +91,7 @@ "dist/credentials/Msg91Api.credentials.js", "dist/credentials/MySql.credentials.js", "dist/credentials/NextCloudApi.credentials.js", + "dist/credentials/OAuth1Api.credentials.js", "dist/credentials/OAuth2Api.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", "dist/credentials/PagerDutyApi.credentials.js", @@ -114,6 +115,7 @@ "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TrelloApi.credentials.js", "dist/credentials/TwilioApi.credentials.js", + "dist/credentials/TwitterOAuth1Api.credentials.js", "dist/credentials/TypeformApi.credentials.js", "dist/credentials/TogglApi.credentials.js", "dist/credentials/UpleadApi.credentials.js", @@ -261,6 +263,7 @@ "dist/nodes/Trello/Trello.node.js", "dist/nodes/Trello/TrelloTrigger.node.js", "dist/nodes/Twilio/Twilio.node.js", + "dist/nodes/Twitter/Twitter.node.js", "dist/nodes/Typeform/TypeformTrigger.node.js", "dist/nodes/Uplead/Uplead.node.js", "dist/nodes/Vero/Vero.node.js",