mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 21:19:43 -08:00
Merge branch 'master' of https://github.com/n8n-io/n8n
This commit is contained in:
commit
8ee60e471b
|
@ -3,6 +3,21 @@
|
||||||
This list shows all the versions which include breaking changes and how to upgrade.
|
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
|
## 0.68.0
|
||||||
|
|
||||||
### What changed?
|
### What changed?
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -29,12 +29,6 @@ export const nodeBase = mixins(nodeIndex).extend({
|
||||||
isMacOs (): boolean {
|
isMacOs (): boolean {
|
||||||
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
|
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 {
|
nodeName (): string {
|
||||||
return NODE_NAME_PREFIX + this.nodeIndex;
|
return NODE_NAME_PREFIX + this.nodeIndex;
|
||||||
},
|
},
|
||||||
|
@ -276,11 +270,18 @@ export const nodeBase = mixins(nodeIndex).extend({
|
||||||
this.instance.addEndpoint(this.nodeName, newEndpointData);
|
this.instance.addEndpoint(this.nodeName, newEndpointData);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isReadOnly === false) {
|
// TODO: This caused problems with displaying old information
|
||||||
|
// https://github.com/jsplumb/katavorio/wiki
|
||||||
|
// https://jsplumb.github.io/jsplumb/home.html
|
||||||
// Make nodes draggable
|
// Make nodes draggable
|
||||||
this.instance.draggable(this.nodeName, {
|
this.instance.draggable(this.nodeName, {
|
||||||
grid: [10, 10],
|
grid: [10, 10],
|
||||||
start: (params: { e: MouseEvent }) => {
|
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)) {
|
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
|
// 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
|
// undefined. So check if the currently dragged node is selected and if not clear
|
||||||
|
@ -290,6 +291,7 @@ export const nodeBase = mixins(nodeIndex).extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit('addActiveAction', 'dragActive');
|
this.$store.commit('addActiveAction', 'dragActive');
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
stop: (params: { e: MouseEvent }) => {
|
stop: (params: { e: MouseEvent }) => {
|
||||||
if (this.$store.getters.isActionActive('dragActive')) {
|
if (this.$store.getters.isActionActive('dragActive')) {
|
||||||
|
@ -332,7 +334,7 @@ export const nodeBase = mixins(nodeIndex).extend({
|
||||||
},
|
},
|
||||||
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
|
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
|
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
|
||||||
|
|
|
@ -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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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<INodeExecutionData[][]> {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
const responseData = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions);
|
|
||||||
return [this.helpers.returnJsonArray(responseData)];
|
|
||||||
} else {
|
|
||||||
throw new Error('Unknown operation');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.',
|
||||||
|
@ -70,54 +75,10 @@ export const tweetFields = [
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
displayName: 'Attachments',
|
displayName: 'Attachments',
|
||||||
name: 'attachmentsUi',
|
name: 'attachments',
|
||||||
placeholder: 'Add Attachments',
|
|
||||||
type: 'fixedCollection',
|
|
||||||
typeOptions: {
|
|
||||||
multipleValues: true,
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'attachment',
|
|
||||||
displayName: 'Attachment',
|
|
||||||
values: [
|
|
||||||
{
|
|
||||||
displayName: 'Binary Property',
|
|
||||||
name: 'binaryPropertyName',
|
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'data',
|
default: 'data',
|
||||||
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<br />data which should be added to tweet as attachment.<br />Multiple ones can be comma separated.',
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
displayName: 'Display Coordinates',
|
||||||
|
@ -167,4 +128,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[];
|
||||||
|
|
|
@ -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[] = [];
|
||||||
|
@ -86,17 +109,18 @@ export class Twitter implements INodeType {
|
||||||
status: text,
|
status: text,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (additionalFields.attachmentsUi) {
|
if (additionalFields.attachments) {
|
||||||
const mediaIds = [];
|
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';
|
const uploadUri = 'https://upload.twitter.com/1.1/media/upload.json';
|
||||||
|
|
||||||
if (attachmentsUi.attachment) {
|
const attachmentProperties: string[] = attachments.split(',').map((propertyName) => {
|
||||||
const attachtments = attachmentsUi.attachment as IDataObject[];
|
return propertyName.trim();
|
||||||
for (const attachment of attachtments) {
|
});
|
||||||
|
|
||||||
|
for (const binaryPropertyName of attachmentProperties) {
|
||||||
|
|
||||||
const binaryData = items[i].binary as IBinaryKeyData;
|
const binaryData = items[i].binary as IBinaryKeyData;
|
||||||
const binaryPropertyName = attachment.binaryPropertyName as string;
|
|
||||||
|
|
||||||
if (binaryData === undefined) {
|
if (binaryData === undefined) {
|
||||||
throw new Error('No binary data set. So file can not be written!');
|
throw new Error('No binary data set. So file can not be written!');
|
||||||
|
@ -106,13 +130,75 @@ 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);
|
|
||||||
mediaIds.push(response.media_id_string);
|
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(',');
|
body.media_ids = mediaIds.join(',');
|
||||||
|
@ -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[]);
|
||||||
|
|
|
@ -321,6 +321,7 @@
|
||||||
"glob-promise": "^3.4.0",
|
"glob-promise": "^3.4.0",
|
||||||
"gm": "^1.23.1",
|
"gm": "^1.23.1",
|
||||||
"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",
|
||||||
|
|
Loading…
Reference in a new issue