Improvements

This commit is contained in:
ricardo 2020-06-07 17:29:13 -04:00
parent aff24bb1a5
commit 5e93b37c6e
8 changed files with 402 additions and 38 deletions

View file

@ -219,12 +219,17 @@ export function requestOAuth1(this: IAllExecuteFunctions, credentialsType: strin
//@ts-ignore //@ts-ignore
url: requestOptions.url, url: requestOptions.url,
method: requestOptions.method, method: requestOptions.method,
data: requestOptions.body, data: { ...requestOptions.qs, ...requestOptions.body },
json: requestOptions.json, json: requestOptions.json,
}; };
if (Object.keys(requestOptions.qs).length !== 0) {
//@ts-ignore
newRequestOptions.qs = oauth.authorize(newRequestOptions as RequestOptions, token);
} else {
//@ts-ignore //@ts-ignore
newRequestOptions.form = oauth.authorize(newRequestOptions as RequestOptions, token); newRequestOptions.form = oauth.authorize(newRequestOptions as RequestOptions, token);
}
return this.helpers.request!(newRequestOptions) return this.helpers.request!(newRequestOptions)
.catch(async (error: IResponseError) => { .catch(async (error: IResponseError) => {

View file

@ -50,7 +50,7 @@ export async function githubApiRequest(this: IHookFunctions | IExecuteFunctions,
const baseUrl = credentials!.server || 'https://api.github.com'; const baseUrl = credentials!.server || 'https://api.github.com';
options.uri = `${baseUrl}${endpoint}`; options.uri = `${baseUrl}${endpoint}`;
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'githubOAuth2Api', options); return await this.helpers.requestOAuth2.call(this, 'githubOAuth2Api', options);
} }
} catch (error) { } catch (error) {

View file

@ -801,6 +801,7 @@ export class HttpRequest implements INodeType {
// Now that the options are all set make the actual http request // Now that the options are all set make the actual http request
if (oAuth2Api !== undefined) { if (oAuth2Api !== undefined) {
//@ts-ignore
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions); response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions);
} else { } else {
response = await this.helpers.request(requestOptions); response = await this.helpers.request(requestOptions);

View file

@ -137,7 +137,7 @@ export class OAuth implements INodeType {
uri: url, uri: url,
json: true, json: true,
}; };
//@ts-ignore
const responseData = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions); const responseData = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions);
return [this.helpers.returnJsonArray(responseData)]; return [this.helpers.returnJsonArray(responseData)];
} else { } else {

View file

@ -42,3 +42,35 @@ export async function twitterApiRequest(this: IExecuteFunctions | IExecuteSingle
throw error; throw error;
} }
} }
export async function twitterApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // 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;
}

View file

@ -20,6 +20,11 @@ export const tweetOperations = [
value: 'create', value: 'create',
description: 'Create a new tweet', description: 'Create a new tweet',
}, },
{
name: 'Search',
value: 'search',
description: 'Search tweets',
},
], ],
default: 'create', default: 'create',
description: 'The operation to perform.', description: 'The operation to perform.',
@ -88,31 +93,31 @@ export const tweetFields = [
default: '', default: '',
description: 'Name of the binary properties which contain data which should be added to tweet as attachment', description: 'Name of the binary properties which contain data which should be added to tweet as attachment',
}, },
{ // {
displayName: 'Category', // displayName: 'Category',
name: 'category', // name: 'category',
type: 'options', // type: 'options',
options: [ // options: [
{ // {
name: 'Amplify Video', // name: 'Amplify Video',
value: 'amplifyVideo', // value: 'amplifyVideo',
}, // },
{ // {
name: 'Gif', // name: 'Gif',
value: 'tweetGif', // value: 'tweetGif',
}, // },
{ // {
name: 'Image', // name: 'Image',
value: 'tweetImage', // value: 'tweetImage',
}, // },
{ // {
name: 'Video', // name: 'Video',
value: 'tweetVideo', // value: 'tweetVideo',
}, // },
], // ],
default: '', // default: '',
description: 'The category that represents how the media will be used', // 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,</br>
including operators. Queries may additionally be limited by complexity.</br>
Check the searching examples <a href="https://developer.twitter.com/en/docs/tweets/search/guides/standard-operators">here</a>.`,
},
{
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[]; ] as INodeProperties[];

View file

@ -1,6 +1,8 @@
import { import {
IExecuteFunctions, IExecuteFunctions,
ILoadOptionsFunctions,
BINARY_ENCODING,
} from 'n8n-core'; } from 'n8n-core';
import { import {
@ -9,6 +11,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
INodePropertyOptions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -17,16 +20,16 @@ import {
} from './TweetDescription'; } from './TweetDescription';
import { import {
chunks,
twitterApiRequest, twitterApiRequest,
twitterApiRequestAllItems,
} from './GenericFunctions'; } from './GenericFunctions';
import { import {
ITweet, ITweet,
} from './TweetInterface'; } from './TweetInterface';
import { const ISO6391 = require('iso-639-1');
snakeCase,
} from 'change-case';
export class Twitter implements INodeType { export class Twitter implements INodeType {
description: INodeTypeDescription = { 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<INodePropertyOptions[]> {
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<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
const returnData: IDataObject[] = []; const returnData: IDataObject[] = [];
@ -106,11 +129,74 @@ export class Twitter implements INodeType {
continue; continue;
} }
let attachmentBody = {};
let response: IDataObject = {};
if (binaryData[binaryPropertyName].mimeType.includes('image')) {
const attachmentBody = { const attachmentBody = {
media_data: binaryData[binaryPropertyName].data, media_data: binaryData[binaryPropertyName].data,
media_category: snakeCase(attachment.category as string).toUpperCase(),
}; };
const response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri);
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); 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); 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)) { if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]); returnData.push.apply(returnData, responseData as IDataObject[]);

View file

@ -321,6 +321,7 @@
"gm": "^1.23.1", "gm": "^1.23.1",
"googleapis": "~50.0.0", "googleapis": "~50.0.0",
"imap-simple": "^4.3.0", "imap-simple": "^4.3.0",
"iso-639-1": "^2.1.3",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",