first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,103 @@
import type { INodeProperties } from 'n8n-workflow';
export const directMessageOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['directMessage'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Send a direct message to a user',
action: 'Create Direct Message',
},
],
default: 'create',
},
];
export const directMessageFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* directMessage:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'User',
name: 'user',
type: 'resourceLocator',
default: { mode: 'username', value: '' },
required: true,
description: 'The user you want to send the message to',
displayOptions: {
show: {
operation: ['create'],
resource: ['directMessage'],
},
},
modes: [
{
displayName: 'By Username',
name: 'username',
type: 'string',
validation: [],
placeholder: 'e.g. n8n',
url: '',
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [],
placeholder: 'e.g. 1068479892537384960',
url: '',
},
],
},
{
displayName: 'Text',
name: 'text',
type: 'string',
required: true,
default: '',
typeOptions: {
rows: 2,
},
displayOptions: {
show: {
operation: ['create'],
resource: ['directMessage'],
},
},
description:
'The text of the direct message. URL encoding is required. Max length of 10,000 characters.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: ['create'],
resource: ['directMessage'],
},
},
options: [
{
displayName: 'Attachment ID',
name: 'attachments',
type: 'string',
default: '',
placeholder: '1664279886239010824',
description: 'The attachment ID to associate with the message',
},
],
},
];
@@ -0,0 +1,134 @@
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
INodeParameterResourceLocator,
JsonObject,
IRequestOptions,
IHttpRequestMethods,
} from 'n8n-workflow';
import { ApplicationError, NodeApiError, NodeOperationError } from 'n8n-workflow';
export async function twitterApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions,
method: IHttpRequestMethods,
resource: string,
body: IDataObject = {},
qs: IDataObject = {},
fullOutput?: boolean,
uri?: string,
option: IDataObject = {},
) {
let options: IRequestOptions = {
method,
body,
qs,
url: uri || `https://api.twitter.com/2${resource}`,
json: true,
};
try {
if (Object.keys(option).length !== 0) {
options = Object.assign({}, options, option);
}
if (Object.keys(body).length === 0) {
delete options.body;
}
if (Object.keys(qs).length === 0) {
delete options.qs;
}
if (fullOutput) {
return await this.helpers.requestOAuth2.call(this, 'twitterOAuth2Api', options);
} else {
const { data } = await this.helpers.requestOAuth2.call(this, 'twitterOAuth2Api', options);
return data;
}
} catch (error) {
if (error.error?.required_enrollment === 'Appropriate Level of API Access') {
throw new NodeOperationError(
this.getNode(),
'The operation requires Twitter Api to be either Basic or Pro.',
);
} else if (error.errors && error.error?.errors[0].message.includes('must be ')) {
throw new NodeOperationError(this.getNode(), error.error.errors[0].message as string);
}
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function twitterApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
query: IDataObject = {},
) {
const returnData: IDataObject[] = [];
let responseData;
query.max_results = 10;
do {
responseData = await twitterApiRequest.call(this, method, endpoint, body, query, true);
query.next_token = responseData.meta.next_token as string;
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
} while (responseData.meta.next_token);
return returnData;
}
export function returnId(tweetId: INodeParameterResourceLocator) {
if (tweetId.mode === 'id') {
return tweetId.value as string;
} else if (tweetId.mode === 'url') {
try {
const url = new URL(tweetId.value as string);
if (!/(twitter|x).com$/.test(url.hostname)) {
throw new ApplicationError('Invalid domain');
}
const parts = url.pathname.split('/');
if (parts.length !== 4 || parts[2] !== 'status' || !/^\d+$/.test(parts[3])) {
throw new ApplicationError('Invalid path');
}
return parts[3];
} catch (error) {
throw new ApplicationError('Not a valid tweet url', { level: 'warning', cause: error });
}
} else {
throw new ApplicationError(`The mode ${tweetId.mode} is not valid!`, { level: 'warning' });
}
}
export async function returnIdFromUsername(
this: IExecuteFunctions,
usernameRlc: INodeParameterResourceLocator,
) {
usernameRlc.value = (usernameRlc.value as string).includes('@')
? (usernameRlc.value as string).replace('@', '')
: usernameRlc.value;
if (
usernameRlc.mode === 'username' ||
(usernameRlc.mode === 'name' && this.getNode().parameters.list !== undefined)
) {
const user = (await twitterApiRequest.call(
this,
'GET',
`/users/by/username/${usernameRlc.value}`,
{},
)) as { id: string };
return user.id;
} else if (this.getNode().parameters.list === undefined) {
const list = (await twitterApiRequest.call(
this,
'GET',
`/list/by/name/${usernameRlc.value}`,
{},
)) as { id: string };
return list.id;
} else
throw new ApplicationError(`The username mode ${usernameRlc.mode} is not valid!`, {
level: 'warning',
});
}
@@ -0,0 +1,94 @@
import type { INodeProperties } from 'n8n-workflow';
export const listOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['list'],
},
},
options: [
{
name: 'Add Member',
value: 'add',
description: 'Add a member to a list',
action: 'Add Member to List',
},
],
default: 'add',
},
];
export const listFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* list:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'List',
name: 'list',
type: 'resourceLocator',
default: { mode: 'id', value: '' },
required: true,
description: 'The list you want to add the user to',
displayOptions: {
show: {
operation: ['add'],
resource: ['list'],
},
},
modes: [
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [],
placeholder: 'e.g. 99923132',
url: '',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
validation: [],
placeholder: 'e.g. https://twitter.com/i/lists/99923132',
url: '',
},
],
},
{
displayName: 'User',
name: 'user',
type: 'resourceLocator',
default: { mode: 'username', value: '' },
required: true,
description: 'The user you want to add to the list',
displayOptions: {
show: {
operation: ['add'],
resource: ['list'],
},
},
modes: [
{
displayName: 'By Username',
name: 'username',
type: 'string',
validation: [],
placeholder: 'e.g. n8n',
url: '',
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [],
placeholder: 'e.g. 1068479892537384960',
url: '',
},
],
},
];
@@ -0,0 +1,479 @@
import type { INodeProperties } from 'n8n-workflow';
export const tweetOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['tweet'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create, quote, or reply to a tweet',
action: 'Create Tweet',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a tweet',
action: 'Delete Tweet',
},
{
name: 'Like',
value: 'like',
description: 'Like a tweet',
action: 'Like Tweet',
},
{
name: 'Retweet',
value: 'retweet',
description: 'Retweet a tweet',
action: 'Retweet Tweet',
},
{
name: 'Search',
value: 'search',
description: 'Search for tweets from the last seven days',
action: 'Search Tweets',
},
],
default: 'create',
},
];
export const tweetFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* tweet:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Text',
name: 'text',
type: 'string',
typeOptions: {
rows: 2,
},
default: '',
required: true,
displayOptions: {
show: {
operation: ['create'],
resource: ['tweet'],
},
},
description:
'The text of the status update. URLs must be encoded. Links wrapped with the t.co shortener will affect character count',
},
{
displayName: 'Options',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: ['create'],
resource: ['tweet'],
},
},
options: [
{
displayName: 'Location ID',
name: 'location',
type: 'string',
placeholder: '4e696bef7e24d378',
default: '',
description: 'Location information for the tweet',
},
{
displayName: 'Media ID',
name: 'attachments',
type: 'string',
default: '',
placeholder: '1664279886239010824',
description: 'The attachment ID to associate with the message',
},
{
displayName: 'Quote a Tweet',
name: 'inQuoteToStatusId',
type: 'resourceLocator',
default: { mode: 'id', value: '' },
description: 'The tweet being quoted',
modes: [
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [],
placeholder: 'e.g. 1187836157394112513',
url: '',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
validation: [],
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
url: '',
},
],
},
{
displayName: 'Reply to Tweet',
name: 'inReplyToStatusId',
type: 'resourceLocator',
default: { mode: 'id', value: '' },
// required: true,
description: 'The tweet being replied to',
modes: [
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [],
placeholder: 'e.g. 1187836157394112513',
url: '',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
validation: [],
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
url: '',
},
],
},
],
},
{
displayName: 'Locations are not supported due to Twitter V2 API limitations',
name: 'noticeLocation',
type: 'notice',
displayOptions: {
show: {
'/additionalFields.location': [''],
},
},
default: '',
},
{
displayName: 'Attachements are not supported due to Twitter V2 API limitations',
name: 'noticeAttachments',
type: 'notice',
displayOptions: {
show: {
'/additionalFields.attachments': [''],
},
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* tweet:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Tweet',
name: 'tweetDeleteId',
type: 'resourceLocator',
default: { mode: 'id', value: '' },
required: true,
description: 'The tweet to delete',
displayOptions: {
show: {
resource: ['tweet'],
operation: ['delete'],
},
},
modes: [
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [],
placeholder: 'e.g. 1187836157394112513',
url: '',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
validation: [],
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
url: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* tweet:like */
/* -------------------------------------------------------------------------- */
{
displayName: 'Tweet',
name: 'tweetId',
type: 'resourceLocator',
default: { mode: 'id', value: '' },
required: true,
description: 'The tweet to like',
displayOptions: {
show: {
operation: ['like'],
resource: ['tweet'],
},
},
modes: [
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [],
placeholder: 'e.g. 1187836157394112513',
url: '',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
validation: [],
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
url: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* tweet:search */
/* -------------------------------------------------------------------------- */
{
// displayName: 'Search Text',
displayName: 'Search Term',
name: 'searchText',
type: 'string',
required: true,
default: '',
placeholder: 'e.g. automation',
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 <a href="https://developer.twitter.com/en/docs/tweets/search/guides/standard-operators">here</a>.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
resource: ['tweet'],
operation: ['search'],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
description: 'Max number of results to return',
typeOptions: {
minValue: 1,
},
displayOptions: {
show: {
resource: ['tweet'],
operation: ['search'],
returnAll: [false],
},
},
},
{
displayName: 'Options',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: ['search'],
resource: ['tweet'],
},
},
options: [
{
displayName: 'Sort Order',
name: 'sortOrder',
type: 'options',
options: [
{
name: 'Recent',
value: 'recency',
},
{
name: 'Relevant',
value: 'relevancy',
},
],
// required: true,
description: 'The order in which to return results',
default: 'recency',
},
{
displayName: 'After',
name: 'startTime',
type: 'dateTime',
default: '',
description:
"Tweets before this date will not be returned. This date must be within the last 7 days if you don't have Academic Research access.",
},
{
displayName: 'Before',
name: 'endTime',
type: 'dateTime',
default: '',
description:
"Tweets after this date will not be returned. This date must be within the last 7 days if you don't have Academic Research access.",
},
{
displayName: 'Tweet Fields',
name: 'tweetFieldsObject',
type: 'multiOptions',
// eslint-disable-next-line n8n-nodes-base/node-param-multi-options-type-unsorted-items
options: [
{
name: 'Attachments',
value: 'attachments',
},
{
name: 'Author ID',
value: 'author_id',
},
{
name: 'Context Annotations',
value: 'context_annotations',
},
{
name: 'Conversation ID',
value: 'conversation_id',
},
{
name: 'Created At',
value: 'created_at',
},
{
name: 'Edit Controls',
value: 'edit_controls',
},
{
name: 'Entities',
value: 'entities',
},
{
name: 'Geo',
value: 'geo',
},
{
name: 'ID',
value: 'id',
},
{
name: 'In Reply To User ID',
value: 'in_reply_to_user_id',
},
{
name: 'Lang',
value: 'lang',
},
{
name: 'Non Public Metrics',
value: 'non_public_metrics',
},
{
name: 'Public Metrics',
value: 'public_metrics',
},
{
name: 'Organic Metrics',
value: 'organic_metrics',
},
{
name: 'Promoted Metrics',
value: 'promoted_metrics',
},
{
name: 'Possibly Sensitive',
value: 'possibly_sensitive',
},
{
name: 'Referenced Tweets',
value: 'referenced_tweets',
},
{
name: 'Reply Settings',
value: 'reply_settings',
},
{
name: 'Source',
value: 'source',
},
{
name: 'Text',
value: 'text',
},
{
name: 'Withheld',
value: 'withheld',
},
],
default: [],
description:
'The fields to add to each returned tweet object. Default fields are: ID, text, edit_history_tweet_ids.',
},
],
},
/* -------------------------------------------------------------------------- */
/* tweet:retweet */
/* -------------------------------------------------------------------------- */
{
displayName: 'Tweet',
name: 'tweetId',
type: 'resourceLocator',
default: { mode: 'id', value: '' },
required: true,
description: 'The tweet to retweet',
displayOptions: {
show: {
operation: ['retweet'],
resource: ['tweet'],
},
},
modes: [
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [],
placeholder: 'e.g. 1187836157394112513',
url: '',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
validation: [],
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
url: '',
},
],
},
];
@@ -0,0 +1,25 @@
import type { IDataObject } from 'n8n-workflow';
export interface ITweet {
auto_populate_reply_metadata?: boolean;
display_coordinates?: boolean;
lat?: number;
long?: number;
media_ids?: string;
possibly_sensitive?: boolean;
status: string;
in_reply_to_status_id?: string;
}
export interface ITweetCreate {
type: 'message_create';
message_create: {
target: {
recipient_id: string;
};
message_data: {
text: string;
attachment?: IDataObject;
};
};
}
@@ -0,0 +1,366 @@
import ISO6391 from 'iso-639-1';
import { DateTime } from 'luxon';
import {
NodeConnectionTypes,
type IDataObject,
type IExecuteFunctions,
type ILoadOptionsFunctions,
type INodeExecutionData,
type INodeParameterResourceLocator,
type INodePropertyOptions,
type INodeType,
type INodeTypeBaseDescription,
type INodeTypeDescription,
type JsonObject,
} from 'n8n-workflow';
import { directMessageFields, directMessageOperations } from './DirectMessageDescription';
import {
returnId,
returnIdFromUsername,
twitterApiRequest,
twitterApiRequestAllItems,
} from './GenericFunctions';
import { listFields, listOperations } from './ListDescription';
import { tweetFields, tweetOperations } from './TweetDescription';
import { userFields, userOperations } from './UserDescription';
export class TwitterV2 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
version: 2,
description:
'Post, like, and search tweets, send messages, search users, and add users to lists',
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
defaults: {
name: 'X',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'twitterOAuth2Api',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Direct Message',
value: 'directMessage',
description: 'Send a direct message to a user',
},
{
name: 'List',
value: 'list',
description: 'Add a user to a list',
},
{
name: 'Tweet',
value: 'tweet',
description: 'Create, like, search, or delete a tweet',
},
{
name: 'User',
value: 'user',
description: 'Search users by username',
},
],
default: 'tweet',
},
// DIRECT MESSAGE
...directMessageOperations,
...directMessageFields,
// LIST
...listOperations,
...listFields,
// TWEET
...tweetOperations,
...tweetFields,
// USER
...userOperations,
...userFields,
],
};
}
methods = {
loadOptions: {
// Get all the available languages to display them to user so that they 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[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < length; i++) {
try {
if (resource === 'user') {
if (operation === 'searchUser') {
const me = this.getNodeParameter('me', i, false) as boolean;
if (me) {
responseData = await twitterApiRequest.call(this, 'GET', '/users/me', {});
} else {
const userRlc = this.getNodeParameter(
'user',
i,
undefined,
{},
) as INodeParameterResourceLocator;
if (userRlc.mode === 'username') {
userRlc.value = (userRlc.value as string).includes('@')
? (userRlc.value as string).replace('@', '')
: userRlc.value;
responseData = await twitterApiRequest.call(
this,
'GET',
`/users/by/username/${userRlc.value}`,
{},
);
} else if (userRlc.mode === 'id') {
responseData = await twitterApiRequest.call(
this,
'GET',
`/users/${userRlc.value}`,
{},
);
}
}
}
}
if (resource === 'tweet') {
if (operation === 'search') {
const searchText = this.getNodeParameter('searchText', i, '', {});
const returnAll = this.getNodeParameter('returnAll', i);
const { sortOrder, startTime, endTime, tweetFieldsObject } = this.getNodeParameter(
'additionalFields',
i,
{},
) as {
sortOrder: string;
startTime: string;
endTime: string;
tweetFieldsObject: string[];
};
const qs: IDataObject = {
query: searchText,
};
if (endTime) {
const endTimeISO = DateTime.fromISO(endTime).toISO();
qs.end_time = endTimeISO;
}
if (sortOrder) {
qs.sort_order = sortOrder;
}
if (startTime) {
const startTimeISO8601 = DateTime.fromISO(startTime).toISO();
qs.start_time = startTimeISO8601;
}
if (tweetFieldsObject) {
if (tweetFieldsObject.length > 0) {
qs['tweet.fields'] = tweetFieldsObject.join(',');
}
}
if (returnAll) {
responseData = await twitterApiRequestAllItems.call(
this,
'data',
'GET',
'/tweets/search/recent',
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i);
qs.max_results = limit;
responseData = await twitterApiRequest.call(
this,
'GET',
'/tweets/search/recent',
{},
qs,
);
}
}
if (operation === 'create') {
const text = this.getNodeParameter('text', i, '', {});
const { location, attachments, inQuoteToStatusId, inReplyToStatusId } =
this.getNodeParameter('additionalFields', i, {}) as {
location: string;
attachments: string;
inQuoteToStatusId: INodeParameterResourceLocator;
inReplyToStatusId: INodeParameterResourceLocator;
};
const body: IDataObject = {
text,
};
if (location) {
body.geo = { place_id: location };
}
if (attachments) {
body.media = { media_ids: [attachments] };
}
if (inQuoteToStatusId) {
body.quote_tweet_id = returnId(inQuoteToStatusId);
}
if (inReplyToStatusId) {
const inReplyToStatusIdValue = { in_reply_to_tweet_id: returnId(inReplyToStatusId) };
body.reply = inReplyToStatusIdValue;
}
responseData = await twitterApiRequest.call(this, 'POST', '/tweets', body);
}
if (operation === 'delete') {
const tweetRLC = this.getNodeParameter(
'tweetDeleteId',
i,
'',
{},
) as INodeParameterResourceLocator;
const tweetId = returnId(tweetRLC);
responseData = await twitterApiRequest.call(this, 'DELETE', `/tweets/${tweetId}`, {});
}
if (operation === 'like') {
const tweetRLC = this.getNodeParameter(
'tweetId',
i,
'',
{},
) as INodeParameterResourceLocator;
const tweetId = returnId(tweetRLC);
const body: IDataObject = {
tweet_id: tweetId,
};
const user = (await twitterApiRequest.call(this, 'GET', '/users/me', {})) as {
id: string;
};
responseData = await twitterApiRequest.call(
this,
'POST',
`/users/${user.id}/likes`,
body,
);
}
if (operation === 'retweet') {
const tweetRLC = this.getNodeParameter(
'tweetId',
i,
'',
{},
) as INodeParameterResourceLocator;
const tweetId = returnId(tweetRLC);
const body: IDataObject = {
tweet_id: tweetId,
};
const user = (await twitterApiRequest.call(this, 'GET', '/users/me', {})) as {
id: string;
};
responseData = await twitterApiRequest.call(
this,
'POST',
`/users/${user.id}/retweets`,
body,
);
}
}
if (resource === 'list') {
if (operation === 'add') {
const userRlc = this.getNodeParameter(
'user',
i,
'',
{},
) as INodeParameterResourceLocator;
const userId =
userRlc.mode !== 'username'
? returnId(userRlc)
: await returnIdFromUsername.call(this, userRlc);
const listRlc = this.getNodeParameter(
'list',
i,
'',
{},
) as INodeParameterResourceLocator;
const listId = returnId(listRlc);
responseData = await twitterApiRequest.call(this, 'POST', `/lists/${listId}/members`, {
user_id: userId,
});
}
}
if (resource === 'directMessage') {
if (operation === 'create') {
const userRlc = this.getNodeParameter(
'user',
i,
'',
{},
) as INodeParameterResourceLocator;
const user = await returnIdFromUsername.call(this, userRlc);
const text = this.getNodeParameter('text', i, '', {});
const { attachments } = this.getNodeParameter('additionalFields', i, {}, {}) as {
attachments: number;
};
const body: IDataObject = {
text,
};
if (attachments) {
body.attachments = [{ media_id: attachments }];
}
responseData = await twitterApiRequest.call(
this,
'POST',
`/dm_conversations/with/${user}/messages`,
body,
);
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = {
json: {
error: (error as JsonObject).message,
},
};
returnData.push(executionErrorData);
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,78 @@
import type { INodeProperties } from 'n8n-workflow';
export const userOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['user'],
},
},
options: [
{
name: 'Get',
value: 'searchUser',
description: 'Retrieve a user by username',
action: 'Get User',
},
],
default: 'searchUser',
},
];
export const userFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* user:searchUser */
/* -------------------------------------------------------------------------- */
{
displayName: 'User',
name: 'user',
type: 'resourceLocator',
default: { mode: 'username', value: '' },
required: true,
description: 'The user you want to search',
displayOptions: {
show: {
operation: ['searchUser'],
resource: ['user'],
},
hide: {
me: [true],
},
},
modes: [
{
displayName: 'By Username',
name: 'username',
type: 'string',
validation: [],
placeholder: 'e.g. n8n',
url: '',
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [],
placeholder: 'e.g. 1068479892537384960',
url: '',
},
],
},
{
displayName: 'Me',
name: 'me',
type: 'boolean',
displayOptions: {
show: {
operation: ['searchUser'],
resource: ['user'],
},
},
default: false,
description: 'Whether you want to search the authenticated user',
},
];