From 5e93b37c6e800856c3d43053fdbe16b9e335cbe4 Mon Sep 17 00:00:00 2001 From: ricardo Date: Sun, 7 Jun 2020 17:29:13 -0400 Subject: [PATCH 1/5] :zap: Improvements --- packages/core/src/NodeExecuteFunctions.ts | 11 +- .../nodes/Github/GenericFunctions.ts | 2 +- packages/nodes-base/nodes/HttpRequest.node.ts | 1 + packages/nodes-base/nodes/OAuth.node.ts | 2 +- .../nodes/Twitter/GenericFunctions.ts | 32 +++ .../nodes/Twitter/TweetDescription.ts | 248 ++++++++++++++++-- .../nodes-base/nodes/Twitter/Twitter.node.ts | 143 +++++++++- packages/nodes-base/package.json | 1 + 8 files changed, 402 insertions(+), 38 deletions(-) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index f0d0d01751..d500d56f88 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -219,12 +219,17 @@ export function requestOAuth1(this: IAllExecuteFunctions, credentialsType: strin //@ts-ignore url: requestOptions.url, method: requestOptions.method, - data: requestOptions.body, + data: { ...requestOptions.qs, ...requestOptions.body }, json: requestOptions.json, }; - //@ts-ignore - newRequestOptions.form = oauth.authorize(newRequestOptions as RequestOptions, token); + if (Object.keys(requestOptions.qs).length !== 0) { + //@ts-ignore + newRequestOptions.qs = oauth.authorize(newRequestOptions as RequestOptions, token); + } else { + //@ts-ignore + newRequestOptions.form = oauth.authorize(newRequestOptions as RequestOptions, token); + } return this.helpers.request!(newRequestOptions) .catch(async (error: IResponseError) => { diff --git a/packages/nodes-base/nodes/Github/GenericFunctions.ts b/packages/nodes-base/nodes/Github/GenericFunctions.ts index 592367019e..de20d34062 100644 --- a/packages/nodes-base/nodes/Github/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Github/GenericFunctions.ts @@ -50,7 +50,7 @@ export async function githubApiRequest(this: IHookFunctions | IExecuteFunctions, const baseUrl = credentials!.server || 'https://api.github.com'; options.uri = `${baseUrl}${endpoint}`; - + //@ts-ignore return await this.helpers.requestOAuth2.call(this, 'githubOAuth2Api', options); } } catch (error) { diff --git a/packages/nodes-base/nodes/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest.node.ts index 499575a728..417d80eeea 100644 --- a/packages/nodes-base/nodes/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest.node.ts @@ -801,6 +801,7 @@ export class HttpRequest implements INodeType { // Now that the options are all set make the actual http request if (oAuth2Api !== undefined) { + //@ts-ignore response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions); } else { response = await this.helpers.request(requestOptions); diff --git a/packages/nodes-base/nodes/OAuth.node.ts b/packages/nodes-base/nodes/OAuth.node.ts index 685b07cc40..3966571cd1 100644 --- a/packages/nodes-base/nodes/OAuth.node.ts +++ b/packages/nodes-base/nodes/OAuth.node.ts @@ -137,7 +137,7 @@ export class OAuth implements INodeType { uri: url, json: true, }; - + //@ts-ignore const responseData = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions); return [this.helpers.returnJsonArray(responseData)]; } else { diff --git a/packages/nodes-base/nodes/Twitter/GenericFunctions.ts b/packages/nodes-base/nodes/Twitter/GenericFunctions.ts index d13ea1c548..1b793047c3 100644 --- a/packages/nodes-base/nodes/Twitter/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Twitter/GenericFunctions.ts @@ -42,3 +42,35 @@ export async function twitterApiRequest(this: IExecuteFunctions | IExecuteSingle throw error; } } + +export async function twitterApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.count = 100; + do { + responseData = await twitterApiRequest.call(this, method, endpoint, body, query); + query.since_id = responseData.search_metadata.max_id; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.search_metadata && + responseData.search_metadata.next_results + ); + + return returnData; +} + +export function chunks (buffer: Buffer, chunkSize: number) { + const result = []; + const len = buffer.length; + let i = 0; + + while (i < len) { + result.push(buffer.slice(i, i += chunkSize)); + } + + return result; +} + + diff --git a/packages/nodes-base/nodes/Twitter/TweetDescription.ts b/packages/nodes-base/nodes/Twitter/TweetDescription.ts index 46e1760e6f..54d3ccbfad 100644 --- a/packages/nodes-base/nodes/Twitter/TweetDescription.ts +++ b/packages/nodes-base/nodes/Twitter/TweetDescription.ts @@ -20,6 +20,11 @@ export const tweetOperations = [ value: 'create', description: 'Create a new tweet', }, + { + name: 'Search', + value: 'search', + description: 'Search tweets', + }, ], default: 'create', description: 'The operation to perform.', @@ -88,31 +93,31 @@ export const tweetFields = [ default: '', description: 'Name of the binary properties which contain data which should be added to tweet 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', - }, + // { + // 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', + // }, ], }, ], @@ -167,4 +172,197 @@ export const tweetFields = [ }, ] }, +/* -------------------------------------------------------------------------- */ +/* tweet:search */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Search Text', + name: 'searchText', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: true, + default: '', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'tweet', + ], + }, + }, + description: `A UTF-8, URL-encoded search query of 500 characters maximum,
+ including operators. Queries may additionally be limited by complexity.
+ Check the searching examples here.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'tweet', + ], + }, + }, + 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: [ + 'search', + ], + resource: [ + 'tweet', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'tweet', + ], + }, + }, + options: [ + { + displayName: 'Include Entities', + name: 'includeEntities', + type: 'boolean', + default: false, + description: 'The entities node will not be included when set to false', + }, + { + displayName: 'Lang', + name: 'lang', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, + default: '', + description: 'Restricts tweets to the given language, given by an ISO 639-1 code. Language detection is best-effort.', + }, + { + 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: 'Radius', + name: 'radius', + type: 'options', + options: [ + { + name: 'Milles', + value: 'mi', + }, + { + name: 'Kilometers', + value: 'km', + }, + ], + required: true, + description: 'Returns tweets by users located within a given radius of the given latitude/longitude.', + default: '', + }, + { + displayName: 'Distance', + name: 'distance', + type: 'number', + typeOptions: { + minValue: 0, + }, + required: true, + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Result Type', + name: 'resultType', + type: 'options', + options: [ + { + name: 'Mixed', + value: 'mixed', + description: 'Include both popular and real time results in the response.', + }, + { + name: 'Recent', + value: 'recent', + description: 'Return only the most recent results in the response', + }, + { + name: 'Popular', + value: 'popular', + description: 'Return only the most popular results in the response.' + }, + ], + default: 'mixed', + description: 'Specifies what type of search results you would prefer to receive', + }, + { + displayName: 'Until', + name: 'until', + type: 'dateTime', + default: '', + description: 'Returns tweets created before the given date', + }, + ], + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twitter/Twitter.node.ts b/packages/nodes-base/nodes/Twitter/Twitter.node.ts index 09b1dcde93..376e9d25d8 100644 --- a/packages/nodes-base/nodes/Twitter/Twitter.node.ts +++ b/packages/nodes-base/nodes/Twitter/Twitter.node.ts @@ -1,6 +1,8 @@ import { IExecuteFunctions, + ILoadOptionsFunctions, + BINARY_ENCODING, } from 'n8n-core'; import { @@ -9,6 +11,7 @@ import { INodeExecutionData, INodeType, INodeTypeDescription, + INodePropertyOptions, } from 'n8n-workflow'; import { @@ -17,16 +20,16 @@ import { } from './TweetDescription'; import { + chunks, twitterApiRequest, + twitterApiRequestAllItems, } from './GenericFunctions'; import { ITweet, } from './TweetInterface'; -import { - snakeCase, -} from 'change-case'; +const ISO6391 = require('iso-639-1'); export class Twitter implements INodeType { description: INodeTypeDescription = { @@ -69,6 +72,26 @@ export class Twitter implements INodeType { ], }; + 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 languages = ISO6391.getAllNames(); + 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 returnData: IDataObject[] = []; @@ -106,11 +129,74 @@ export class Twitter implements INodeType { continue; } - const attachmentBody = { - media_data: binaryData[binaryPropertyName].data, - media_category: snakeCase(attachment.category as string).toUpperCase(), - }; - const response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + let attachmentBody = {}; + let response: IDataObject = {}; + + if (binaryData[binaryPropertyName].mimeType.includes('image')) { + + const attachmentBody = { + media_data: binaryData[binaryPropertyName].data, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + } else { + + // https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-init + + attachmentBody = { + command: 'INIT', + total_bytes: Buffer.from(binaryData[binaryPropertyName].data, BINARY_ENCODING).byteLength, + media_type: binaryData[binaryPropertyName].mimeType, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + const mediaId = response.media_id_string; + + // break the data on 5mb chunks (max size that can be uploaded at once) + + const binaryParts = chunks(Buffer.from(binaryData[binaryPropertyName].data, BINARY_ENCODING), 5242880); + + let index = 0; + + for (const binaryPart of binaryParts) { + + //https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-append + + attachmentBody = { + name: binaryData[binaryPropertyName].fileName, + command: 'APPEND', + media_id: mediaId, + media_data: Buffer.from(binaryPart).toString('base64'), + segment_index: index, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + index++; + } + + //https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-finalize + + attachmentBody = { + command: 'FINALIZE', + media_id: mediaId, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + // data has not been uploaded yet, so wait for it to be ready + if (response.processing_info) { + const { check_after_secs } = (response.processing_info as IDataObject); + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve(); + }, (check_after_secs as number) * 1000); + }); + } + } + mediaIds.push(response.media_id_string); } } @@ -133,6 +219,47 @@ export class Twitter implements INodeType { responseData = await twitterApiRequest.call(this, 'POST', '/statuses/update.json', body); } + // https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets + if (operation === 'search') { + const q = this.getNodeParameter('searchText', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const qs: IDataObject = { + q + }; + + if (additionalFields.includeEntities) { + qs.include_entities = additionalFields.includeEntities as boolean; + } + + if (additionalFields.resultType) { + qs.response_type = additionalFields.resultType as string; + } + + if (additionalFields.until) { + qs.until = additionalFields.until as string; + } + + if (additionalFields.lang) { + qs.lang = additionalFields.lang as string; + } + + if (additionalFields.locationFieldsUi) { + const locationUi = additionalFields.locationFieldsUi as IDataObject; + if (locationUi.locationFieldsValues) { + const values = locationUi.locationFieldsValues as IDataObject; + qs.geocode = `${values.latitude as string},${values.longitude as string},${values.distance}${values.radius}`; + } + } + + if (returnAll) { + responseData = await twitterApiRequestAllItems.call(this, 'statuses', 'GET', '/search/tweets.json', {}, qs); + } else { + qs.count = this.getNodeParameter('limit', 0) as number; + responseData = await twitterApiRequest.call(this, 'GET', '/search/tweets.json', {}, qs); + responseData = responseData.statuses; + } + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 31cb4a8da5..596ba1e499 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -321,6 +321,7 @@ "gm": "^1.23.1", "googleapis": "~50.0.0", "imap-simple": "^4.3.0", + "iso-639-1": "^2.1.3", "jsonwebtoken": "^8.5.1", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", From 9cae58c78798f8d9d2c152e4c5b6e57a20654778 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 8 Jun 2020 00:34:15 +0200 Subject: [PATCH 2/5] :bug: Fix issue that nodes can not be opened in readOnly-Mode --- .../src/components/mixins/nodeBase.ts | 114 +++++++++--------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/packages/editor-ui/src/components/mixins/nodeBase.ts b/packages/editor-ui/src/components/mixins/nodeBase.ts index 03ad84db00..ba3661700a 100644 --- a/packages/editor-ui/src/components/mixins/nodeBase.ts +++ b/packages/editor-ui/src/components/mixins/nodeBase.ts @@ -29,12 +29,6 @@ export const nodeBase = mixins(nodeIndex).extend({ isMacOs (): boolean { return /(ipad|iphone|ipod|mac)/i.test(navigator.platform); }, - isReadOnly (): boolean { - if (['NodeViewExisting', 'NodeViewNew'].includes(this.$route.name as string)) { - return false; - } - return true; - }, nodeName (): string { return NODE_NAME_PREFIX + this.nodeIndex; }, @@ -276,63 +270,71 @@ export const nodeBase = mixins(nodeIndex).extend({ this.instance.addEndpoint(this.nodeName, newEndpointData); }); - if (this.isReadOnly === false) { - // Make nodes draggable - this.instance.draggable(this.nodeName, { - grid: [10, 10], - start: (params: { e: MouseEvent }) => { - if (params.e && !this.$store.getters.isNodeSelected(this.data.name)) { - // Only the node which gets dragged directly gets an event, for all others it is - // undefined. So check if the currently dragged node is selected and if not clear - // the drag-selection. - this.instance.clearDragSelection(); - this.$store.commit('resetSelectedNodes'); + // TODO: This caused problems with displaying old information + // https://github.com/jsplumb/katavorio/wiki + // https://jsplumb.github.io/jsplumb/home.html + // Make nodes draggable + this.instance.draggable(this.nodeName, { + grid: [10, 10], + start: (params: { e: MouseEvent }) => { + if (this.isReadOnly === true) { + // Do not allow to move nodes in readOnly mode + return false; + } + + if (params.e && !this.$store.getters.isNodeSelected(this.data.name)) { + // Only the node which gets dragged directly gets an event, for all others it is + // undefined. So check if the currently dragged node is selected and if not clear + // the drag-selection. + this.instance.clearDragSelection(); + this.$store.commit('resetSelectedNodes'); + } + + this.$store.commit('addActiveAction', 'dragActive'); + return true; + }, + stop: (params: { e: MouseEvent }) => { + if (this.$store.getters.isActionActive('dragActive')) { + const moveNodes = this.$store.getters.getSelectedNodes.slice(); + const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name); + if (!selectedNodeNames.includes(this.data.name)) { + // If the current node is not in selected add it to the nodes which + // got moved manually + moveNodes.push(this.data); } - this.$store.commit('addActiveAction', 'dragActive'); - }, - stop: (params: { e: MouseEvent }) => { - if (this.$store.getters.isActionActive('dragActive')) { - const moveNodes = this.$store.getters.getSelectedNodes.slice(); - const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name); - if (!selectedNodeNames.includes(this.data.name)) { - // If the current node is not in selected add it to the nodes which - // got moved manually - moveNodes.push(this.data); + // This does for some reason just get called once for the node that got clicked + // even though "start" and "drag" gets called for all. So lets do for now + // some dirty DOM query to get the new positions till I have more time to + // create a proper solution + let newNodePositon: XYPositon; + moveNodes.forEach((node: INodeUi) => { + const nodeElement = `node-${this.getNodeIndex(node.name)}`; + const element = document.getElementById(nodeElement); + if (element === null) { + return; } - // This does for some reason just get called once for the node that got clicked - // even though "start" and "drag" gets called for all. So lets do for now - // some dirty DOM query to get the new positions till I have more time to - // create a proper solution - let newNodePositon: XYPositon; - moveNodes.forEach((node: INodeUi) => { - const nodeElement = `node-${this.getNodeIndex(node.name)}`; - const element = document.getElementById(nodeElement); - if (element === null) { - return; - } + newNodePositon = [ + parseInt(element.style.left!.slice(0, -2), 10), + parseInt(element.style.top!.slice(0, -2), 10), + ]; - newNodePositon = [ - parseInt(element.style.left!.slice(0, -2), 10), - parseInt(element.style.top!.slice(0, -2), 10), - ]; + const updateInformation = { + name: node.name, + properties: { + // @ts-ignore, draggable does not have definitions + position: newNodePositon, + }, + }; - const updateInformation = { - name: node.name, - properties: { - // @ts-ignore, draggable does not have definitions - position: newNodePositon, - }, - }; + this.$store.commit('updateNodeProperties', updateInformation); + }); + } + }, + filter: '.node-description, .node-description .node-name, .node-description .node-subtitle', + }); - this.$store.commit('updateNodeProperties', updateInformation); - }); - } - }, - filter: '.node-description, .node-description .node-name, .node-description .node-subtitle', - }); - } }, isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean { From 709de6fd5f575fc5b21315b89320723800eb3b40 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 8 Jun 2020 00:54:45 +0200 Subject: [PATCH 3/5] :zap: Some small improvements to Twitter-Node --- .../nodes/Twitter/TweetDescription.ts | 52 +----- .../nodes-base/nodes/Twitter/Twitter.node.ts | 166 +++++++++--------- 2 files changed, 87 insertions(+), 131 deletions(-) diff --git a/packages/nodes-base/nodes/Twitter/TweetDescription.ts b/packages/nodes-base/nodes/Twitter/TweetDescription.ts index 648e54616e..edd12bbf5e 100644 --- a/packages/nodes-base/nodes/Twitter/TweetDescription.ts +++ b/packages/nodes-base/nodes/Twitter/TweetDescription.ts @@ -75,54 +75,10 @@ export const tweetFields = [ options: [ { displayName: 'Attachments', - name: 'attachmentsUi', - placeholder: 'Add Attachments', - type: 'fixedCollection', - typeOptions: { - multipleValues: true, - }, - options: [ - { - name: 'attachment', - displayName: 'Attachment', - values: [ - { - displayName: 'Binary Property', - name: 'binaryPropertyName', - type: 'string', - default: 'data', - description: 'Name of the binary properties which contain data which should be added to tweet 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.', + name: 'attachments', + type: 'string', + default: 'data', + description: 'Name of the binary properties which contain
data which should be added to tweet as attachment.
Multiple ones can be comma separated.', }, { displayName: 'Display Coordinates', diff --git a/packages/nodes-base/nodes/Twitter/Twitter.node.ts b/packages/nodes-base/nodes/Twitter/Twitter.node.ts index 376e9d25d8..60f95e3798 100644 --- a/packages/nodes-base/nodes/Twitter/Twitter.node.ts +++ b/packages/nodes-base/nodes/Twitter/Twitter.node.ts @@ -109,96 +109,96 @@ export class Twitter implements INodeType { status: text, }; - if (additionalFields.attachmentsUi) { + if (additionalFields.attachments) { const mediaIds = []; - const attachmentsUi = additionalFields.attachmentsUi as IDataObject; + const attachments = additionalFields.attachments as string; const uploadUri = 'https://upload.twitter.com/1.1/media/upload.json'; - if (attachmentsUi.attachment) { - const attachtments = attachmentsUi.attachment as IDataObject[]; - for (const attachment of attachtments) { + const attachmentProperties: string[] = attachments.split(',').map((propertyName) => { + return propertyName.trim(); + }); - const binaryData = items[i].binary as IBinaryKeyData; - const binaryPropertyName = attachment.binaryPropertyName as string; + for (const binaryPropertyName of attachmentProperties) { - if (binaryData === undefined) { - throw new Error('No binary data set. So file can not be written!'); - } + const binaryData = items[i].binary as IBinaryKeyData; - if (!binaryData[binaryPropertyName]) { - continue; - } - - let attachmentBody = {}; - let response: IDataObject = {}; - - if (binaryData[binaryPropertyName].mimeType.includes('image')) { - - const attachmentBody = { - media_data: binaryData[binaryPropertyName].data, - }; - - response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); - - } else { - - // https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-init - - attachmentBody = { - command: 'INIT', - total_bytes: Buffer.from(binaryData[binaryPropertyName].data, BINARY_ENCODING).byteLength, - media_type: binaryData[binaryPropertyName].mimeType, - }; - - response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); - - const mediaId = response.media_id_string; - - // break the data on 5mb chunks (max size that can be uploaded at once) - - const binaryParts = chunks(Buffer.from(binaryData[binaryPropertyName].data, BINARY_ENCODING), 5242880); - - let index = 0; - - for (const binaryPart of binaryParts) { - - //https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-append - - attachmentBody = { - name: binaryData[binaryPropertyName].fileName, - command: 'APPEND', - media_id: mediaId, - media_data: Buffer.from(binaryPart).toString('base64'), - segment_index: index, - }; - - response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); - - index++; - } - - //https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-finalize - - attachmentBody = { - command: 'FINALIZE', - media_id: mediaId, - }; - - response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); - - // data has not been uploaded yet, so wait for it to be ready - if (response.processing_info) { - const { check_after_secs } = (response.processing_info as IDataObject); - await new Promise((resolve, reject) => { - setTimeout(() => { - resolve(); - }, (check_after_secs as number) * 1000); - }); - } - } - - mediaIds.push(response.media_id_string); + if (binaryData === undefined) { + throw new Error('No binary data set. So file can not be written!'); } + + if (!binaryData[binaryPropertyName]) { + continue; + } + + let attachmentBody = {}; + let response: IDataObject = {}; + + if (binaryData[binaryPropertyName].mimeType.includes('image')) { + + const attachmentBody = { + media_data: binaryData[binaryPropertyName].data, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + } else { + + // https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-init + + attachmentBody = { + command: 'INIT', + total_bytes: Buffer.from(binaryData[binaryPropertyName].data, BINARY_ENCODING).byteLength, + media_type: binaryData[binaryPropertyName].mimeType, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + const mediaId = response.media_id_string; + + // break the data on 5mb chunks (max size that can be uploaded at once) + + const binaryParts = chunks(Buffer.from(binaryData[binaryPropertyName].data, BINARY_ENCODING), 5242880); + + let index = 0; + + for (const binaryPart of binaryParts) { + + //https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-append + + attachmentBody = { + name: binaryData[binaryPropertyName].fileName, + command: 'APPEND', + media_id: mediaId, + media_data: Buffer.from(binaryPart).toString('base64'), + segment_index: index, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + index++; + } + + //https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-finalize + + attachmentBody = { + command: 'FINALIZE', + media_id: mediaId, + }; + + response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri); + + // data has not been uploaded yet, so wait for it to be ready + if (response.processing_info) { + const { check_after_secs } = (response.processing_info as IDataObject); + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve(); + }, (check_after_secs as number) * 1000); + }); + } + } + + mediaIds.push(response.media_id_string); } body.media_ids = mediaIds.join(','); From 177c6f65eb8f578de7dd309ccfcd319cc150cc6f Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 8 Jun 2020 09:02:32 +0200 Subject: [PATCH 4/5] :zap: Remove no longer needed OAuth-Test-Files --- .../credentials/TestOAuth2Api.credentials.ts | 26 ---- packages/nodes-base/nodes/OAuth.node.ts | 147 ------------------ 2 files changed, 173 deletions(-) delete mode 100644 packages/nodes-base/credentials/TestOAuth2Api.credentials.ts delete mode 100644 packages/nodes-base/nodes/OAuth.node.ts diff --git a/packages/nodes-base/credentials/TestOAuth2Api.credentials.ts b/packages/nodes-base/credentials/TestOAuth2Api.credentials.ts deleted file mode 100644 index 2a350faecf..0000000000 --- a/packages/nodes-base/credentials/TestOAuth2Api.credentials.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - ICredentialType, - NodePropertyTypes, -} from 'n8n-workflow'; - -const scopes = [ - 'https://www.googleapis.com/auth/calendar', - 'https://www.googleapis.com/auth/calendar.events', -]; - -export class TestOAuth2Api implements ICredentialType { - name = 'testOAuth2Api'; - extends = [ - 'googleOAuth2Api', - ]; - displayName = 'Test OAuth2 API'; - properties = [ - { - displayName: 'Scope', - name: 'scope', - type: 'string' as NodePropertyTypes, - default: '', - placeholder: 'asdf', - }, - ]; -} diff --git a/packages/nodes-base/nodes/OAuth.node.ts b/packages/nodes-base/nodes/OAuth.node.ts deleted file mode 100644 index 3966571cd1..0000000000 --- a/packages/nodes-base/nodes/OAuth.node.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { OptionsWithUri } from 'request'; - -import { IExecuteFunctions } from 'n8n-core'; -import { - INodeExecutionData, - INodeType, - INodeTypeDescription, -} from 'n8n-workflow'; - -export class OAuth implements INodeType { - description: INodeTypeDescription = { - displayName: 'OAuth', - name: 'oauth', - icon: 'fa:code-branch', - group: ['input'], - version: 1, - description: 'Gets, sends data to Oauth API Endpoint and receives generic information.', - defaults: { - name: 'OAuth', - color: '#0033AA', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'oAuth2Api', - required: true, - } - ], - properties: [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - options: [ - { - name: 'Get', - value: 'get', - description: 'Returns the OAuth token data.', - }, - { - name: 'Request', - value: 'request', - description: 'Make an OAuth signed requ.', - }, - ], - default: 'get', - description: 'The operation to perform.', - }, - { - displayName: 'Request Method', - name: 'requestMethod', - type: 'options', - displayOptions: { - show: { - operation: [ - 'request', - ], - }, - }, - options: [ - { - name: 'DELETE', - value: 'DELETE' - }, - { - name: 'GET', - value: 'GET' - }, - { - name: 'HEAD', - value: 'HEAD' - }, - { - name: 'PATCH', - value: 'PATCH' - }, - { - name: 'POST', - value: 'POST' - }, - { - name: 'PUT', - value: 'PUT' - }, - ], - default: 'GET', - description: 'The request method to use.', - }, - { - displayName: 'URL', - name: 'url', - type: 'string', - displayOptions: { - show: { - operation: [ - 'request', - ], - }, - }, - default: '', - placeholder: 'http://example.com/index.html', - description: 'The URL to make the request to.', - required: true, - }, - ] - }; - - async execute(this: IExecuteFunctions): Promise { - const credentials = this.getCredentials('oAuth2Api'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - if (credentials.oauthTokenData === undefined) { - throw new Error('OAuth credentials not connected'); - } - - const operation = this.getNodeParameter('operation', 0) as string; - if (operation === 'get') { - // credentials.oauthTokenData has the refreshToken and accessToken available - // it would be nice to have credentials.getOAuthToken() which returns the accessToken - // and also handles an error case where if the token is to be refreshed, it does so - // without knowledge of the node. - - return [this.helpers.returnJsonArray(JSON.parse(credentials.oauthTokenData as string))]; - } else if (operation === 'request') { - const url = this.getNodeParameter('url', 0) as string; - const requestMethod = this.getNodeParameter('requestMethod', 0) as string; - - // Authorization Code Grant - const requestOptions: OptionsWithUri = { - headers: { - 'User-Agent': 'some-user', - }, - method: requestMethod, - uri: url, - json: true, - }; - //@ts-ignore - const responseData = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions); - return [this.helpers.returnJsonArray(responseData)]; - } else { - throw new Error('Unknown operation'); - } - } -} From 516a56ea320517816f0e8fa7888d516d0749365e Mon Sep 17 00:00:00 2001 From: Tanay Pant <7481165+tanay1337@users.noreply.github.com> Date: Mon, 8 Jun 2020 10:44:52 +0200 Subject: [PATCH 5/5] :books: Add breaking changes for 0.69.0 (#632) --- packages/cli/BREAKING-CHANGES.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 0af22307f3..68be5ce947 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -3,6 +3,21 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.69.0 + +### What changed? + +We have simplified how attachments are handled by the Twitter node. Rather than clicking on `Add Attachments` and having to specify the `Catergory`, you can now add attachments by just clicking on `Add Field` and selecting `Attachments`. There's no longer an option to specify the type of attachment you are adding. + +### When is action necessary? + +If you have used the Attachments option in your Twitter nodes. + +### How to upgrade: + +You'll need to re-create the attachments for the Twitter node. + + ## 0.68.0 ### What changed?