n8n-nodes-twitter-media-upload 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/credentials/TwitterMediaUploadApi.credentials.d.ts +8 -0
- package/dist/credentials/TwitterMediaUploadApi.credentials.js +57 -0
- package/dist/nodes/TwitterMediaUpload/TwitterMediaUpload.node.d.ts +5 -0
- package/dist/nodes/TwitterMediaUpload/TwitterMediaUpload.node.js +243 -0
- package/dist/nodes/TwitterMediaUpload/twitter.svg +22 -0
- package/package.json +56 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { IAuthenticateGeneric, ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
2
|
+
export declare class TwitterMediaUploadApi implements ICredentialType {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
documentationUrl: string;
|
|
6
|
+
properties: INodeProperties[];
|
|
7
|
+
authenticate: IAuthenticateGeneric;
|
|
8
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TwitterMediaUploadApi = void 0;
|
|
4
|
+
class TwitterMediaUploadApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'twitterMediaUploadApi';
|
|
7
|
+
this.displayName = 'Twitter Media Upload API';
|
|
8
|
+
this.documentationUrl = 'https://developer.twitter.com/en/docs/twitter-api';
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: 'Consumer Key',
|
|
12
|
+
name: 'consumerKey',
|
|
13
|
+
type: 'string',
|
|
14
|
+
default: '',
|
|
15
|
+
required: true,
|
|
16
|
+
description: 'Twitter API Consumer Key (API Key)',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
displayName: 'Consumer Secret',
|
|
20
|
+
name: 'consumerSecret',
|
|
21
|
+
type: 'string',
|
|
22
|
+
typeOptions: {
|
|
23
|
+
password: true,
|
|
24
|
+
},
|
|
25
|
+
default: '',
|
|
26
|
+
required: true,
|
|
27
|
+
description: 'Twitter API Consumer Secret (API Secret)',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
displayName: 'Access Token',
|
|
31
|
+
name: 'accessToken',
|
|
32
|
+
type: 'string',
|
|
33
|
+
default: '',
|
|
34
|
+
required: true,
|
|
35
|
+
description: 'Twitter API Access Token',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
displayName: 'Access Token Secret',
|
|
39
|
+
name: 'accessTokenSecret',
|
|
40
|
+
type: 'string',
|
|
41
|
+
typeOptions: {
|
|
42
|
+
password: true,
|
|
43
|
+
},
|
|
44
|
+
default: '',
|
|
45
|
+
required: true,
|
|
46
|
+
description: 'Twitter API Access Token Secret',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
this.authenticate = {
|
|
50
|
+
type: 'generic',
|
|
51
|
+
properties: {
|
|
52
|
+
qs: {},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.TwitterMediaUploadApi = TwitterMediaUploadApi;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class TwitterMediaUpload implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TwitterMediaUpload = void 0;
|
|
7
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
8
|
+
const oauth_1_0a_1 = __importDefault(require("oauth-1.0a"));
|
|
9
|
+
const crypto_js_1 = __importDefault(require("crypto-js"));
|
|
10
|
+
class TwitterMediaUpload {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.description = {
|
|
13
|
+
displayName: 'Twitter Media Upload',
|
|
14
|
+
name: 'twitterMediaUpload',
|
|
15
|
+
icon: 'file:twitter.svg',
|
|
16
|
+
group: ['transform'],
|
|
17
|
+
version: 1,
|
|
18
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
19
|
+
description: 'Upload media files to Twitter/X',
|
|
20
|
+
defaults: {
|
|
21
|
+
name: 'Twitter Media Upload',
|
|
22
|
+
},
|
|
23
|
+
inputs: ['main'],
|
|
24
|
+
outputs: ['main'],
|
|
25
|
+
credentials: [
|
|
26
|
+
{
|
|
27
|
+
name: 'twitterMediaUploadApi',
|
|
28
|
+
required: true,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
properties: [
|
|
32
|
+
{
|
|
33
|
+
displayName: 'Operation',
|
|
34
|
+
name: 'operation',
|
|
35
|
+
type: 'options',
|
|
36
|
+
noDataExpression: true,
|
|
37
|
+
options: [
|
|
38
|
+
{
|
|
39
|
+
name: 'Upload Media',
|
|
40
|
+
value: 'upload',
|
|
41
|
+
description: 'Upload media file to Twitter',
|
|
42
|
+
action: 'Upload media file to Twitter',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
default: 'upload',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
displayName: 'Binary Property',
|
|
49
|
+
name: 'binaryPropertyName',
|
|
50
|
+
type: 'string',
|
|
51
|
+
default: 'data',
|
|
52
|
+
required: true,
|
|
53
|
+
displayOptions: {
|
|
54
|
+
show: {
|
|
55
|
+
operation: ['upload'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
description: 'Name of the binary property which contains the file to upload',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
displayName: 'Media Type',
|
|
62
|
+
name: 'mediaType',
|
|
63
|
+
type: 'options',
|
|
64
|
+
options: [
|
|
65
|
+
{
|
|
66
|
+
name: 'Image (PNG)',
|
|
67
|
+
value: 'image/png',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'Image (JPEG)',
|
|
71
|
+
value: 'image/jpeg',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'Image (GIF)',
|
|
75
|
+
value: 'image/gif',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'Image (WebP)',
|
|
79
|
+
value: 'image/webp',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Video (MP4)',
|
|
83
|
+
value: 'video/mp4',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'Video (MOV)',
|
|
87
|
+
value: 'video/quicktime',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'Custom',
|
|
91
|
+
value: 'custom',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
default: 'image/png',
|
|
95
|
+
displayOptions: {
|
|
96
|
+
show: {
|
|
97
|
+
operation: ['upload'],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
description: 'The MIME type of the media being uploaded',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
displayName: 'Custom Media Type',
|
|
104
|
+
name: 'customMediaType',
|
|
105
|
+
type: 'string',
|
|
106
|
+
default: '',
|
|
107
|
+
displayOptions: {
|
|
108
|
+
show: {
|
|
109
|
+
operation: ['upload'],
|
|
110
|
+
mediaType: ['custom'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
description: 'Custom MIME type (e.g., audio/mpeg)',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async execute() {
|
|
119
|
+
const items = this.getInputData();
|
|
120
|
+
const returnData = [];
|
|
121
|
+
const credentials = await this.getCredentials('twitterMediaUploadApi');
|
|
122
|
+
// Setup OAuth1
|
|
123
|
+
const oauth = new oauth_1_0a_1.default({
|
|
124
|
+
consumer: {
|
|
125
|
+
key: credentials.consumerKey,
|
|
126
|
+
secret: credentials.consumerSecret,
|
|
127
|
+
},
|
|
128
|
+
signature_method: 'HMAC-SHA1',
|
|
129
|
+
hash_function(base_string, key) {
|
|
130
|
+
return crypto_js_1.default.HmacSHA1(base_string, key).toString(crypto_js_1.default.enc.Base64);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const token = {
|
|
134
|
+
key: credentials.accessToken,
|
|
135
|
+
secret: credentials.accessTokenSecret,
|
|
136
|
+
};
|
|
137
|
+
const TWITTER_UPLOAD_URL = 'https://upload.twitter.com/1.1/media/upload.json';
|
|
138
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
139
|
+
try {
|
|
140
|
+
const operation = this.getNodeParameter('operation', itemIndex);
|
|
141
|
+
if (operation === 'upload') {
|
|
142
|
+
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex);
|
|
143
|
+
const mediaType = this.getNodeParameter('mediaType', itemIndex);
|
|
144
|
+
const customMediaType = this.getNodeParameter('customMediaType', itemIndex, '');
|
|
145
|
+
const finalMediaType = mediaType === 'custom' ? customMediaType : mediaType;
|
|
146
|
+
// Get binary data
|
|
147
|
+
const binaryData = this.helpers.assertBinaryData(itemIndex, binaryPropertyName);
|
|
148
|
+
const fileBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName);
|
|
149
|
+
const totalBytes = fileBuffer.length;
|
|
150
|
+
// INIT
|
|
151
|
+
const initData = {
|
|
152
|
+
command: 'INIT',
|
|
153
|
+
total_bytes: totalBytes.toString(),
|
|
154
|
+
media_type: finalMediaType,
|
|
155
|
+
};
|
|
156
|
+
const initRequest = {
|
|
157
|
+
url: TWITTER_UPLOAD_URL,
|
|
158
|
+
method: 'POST',
|
|
159
|
+
data: initData,
|
|
160
|
+
};
|
|
161
|
+
const initAuth = oauth.authorize(initRequest, token);
|
|
162
|
+
const initResponse = await this.helpers.request({
|
|
163
|
+
method: 'POST',
|
|
164
|
+
url: TWITTER_UPLOAD_URL,
|
|
165
|
+
headers: oauth.toHeader(initAuth),
|
|
166
|
+
form: initData,
|
|
167
|
+
json: true,
|
|
168
|
+
});
|
|
169
|
+
const mediaId = initResponse.media_id_string;
|
|
170
|
+
// APPEND
|
|
171
|
+
const appendRequest = {
|
|
172
|
+
url: TWITTER_UPLOAD_URL,
|
|
173
|
+
method: 'POST',
|
|
174
|
+
};
|
|
175
|
+
const appendAuth = oauth.authorize(appendRequest, token);
|
|
176
|
+
await this.helpers.request({
|
|
177
|
+
method: 'POST',
|
|
178
|
+
url: TWITTER_UPLOAD_URL,
|
|
179
|
+
headers: {
|
|
180
|
+
...oauth.toHeader(appendAuth),
|
|
181
|
+
},
|
|
182
|
+
formData: {
|
|
183
|
+
command: 'APPEND',
|
|
184
|
+
media_id: mediaId,
|
|
185
|
+
segment_index: '0',
|
|
186
|
+
media: {
|
|
187
|
+
value: fileBuffer,
|
|
188
|
+
options: {
|
|
189
|
+
filename: binaryData.fileName || 'file',
|
|
190
|
+
contentType: finalMediaType,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
// FINALIZE
|
|
196
|
+
const finalizeData = {
|
|
197
|
+
command: 'FINALIZE',
|
|
198
|
+
media_id: mediaId,
|
|
199
|
+
};
|
|
200
|
+
const finalizeRequest = {
|
|
201
|
+
url: TWITTER_UPLOAD_URL,
|
|
202
|
+
method: 'POST',
|
|
203
|
+
data: finalizeData,
|
|
204
|
+
};
|
|
205
|
+
const finalizeAuth = oauth.authorize(finalizeRequest, token);
|
|
206
|
+
const finalizeResponse = await this.helpers.request({
|
|
207
|
+
method: 'POST',
|
|
208
|
+
url: TWITTER_UPLOAD_URL,
|
|
209
|
+
headers: oauth.toHeader(finalizeAuth),
|
|
210
|
+
form: finalizeData,
|
|
211
|
+
json: true,
|
|
212
|
+
});
|
|
213
|
+
returnData.push({
|
|
214
|
+
json: {
|
|
215
|
+
media_id: mediaId,
|
|
216
|
+
media_id_string: finalizeResponse.media_id_string,
|
|
217
|
+
size: finalizeResponse.size,
|
|
218
|
+
expires_after_secs: finalizeResponse.expires_after_secs,
|
|
219
|
+
media_type: finalMediaType,
|
|
220
|
+
},
|
|
221
|
+
pairedItem: itemIndex,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (this.continueOnFail()) {
|
|
227
|
+
returnData.push({
|
|
228
|
+
json: {
|
|
229
|
+
error: error.message,
|
|
230
|
+
},
|
|
231
|
+
pairedItem: itemIndex,
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, {
|
|
236
|
+
itemIndex,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return [returnData];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
exports.TwitterMediaUpload = TwitterMediaUpload;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<radialGradient id="bg" cx="50%" cy="40%" r="60%">
|
|
4
|
+
<stop offset="0%" stop-color="#1A1A1A"/>
|
|
5
|
+
<stop offset="100%" stop-color="#000000"/>
|
|
6
|
+
</radialGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
|
|
9
|
+
<!-- Rounded dark badge -->
|
|
10
|
+
<rect width="100" height="100" rx="18" fill="url(#bg)"/>
|
|
11
|
+
|
|
12
|
+
<!-- Outer glow ring -->
|
|
13
|
+
<circle cx="50" cy="50" r="46" stroke="#111" stroke-width="2"/>
|
|
14
|
+
|
|
15
|
+
<!-- X Logo -->
|
|
16
|
+
<path d="M26 22 L46 22 L74 78 L54 78 Z" fill="#E6E6E6"/>
|
|
17
|
+
<path d="M54 22 L74 22 L46 78 L26 78 Z" fill="#E6E6E6"/>
|
|
18
|
+
|
|
19
|
+
<!-- Inner cut for futuristic depth -->
|
|
20
|
+
<path d="M34 26 L46 26 L66 74 L54 74 Z" fill="#0B0B0B"/>
|
|
21
|
+
<path d="M54 26 L66 26 L46 74 L34 74 Z" fill="#0B0B0B"/>
|
|
22
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-twitter-media-upload",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "n8n node for uploading media to Twitter/X",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"n8n",
|
|
8
|
+
"twitter",
|
|
9
|
+
"media",
|
|
10
|
+
"upload"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"homepage": "",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Your Name",
|
|
16
|
+
"email": "your.email@example.com"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/deepakdhaka-1/n8n-nodes-twitter-media-upload/"
|
|
21
|
+
},
|
|
22
|
+
"main": "index.js",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc && gulp build:icons",
|
|
25
|
+
"dev": "tsc --watch",
|
|
26
|
+
"format": "prettier nodes credentials --write",
|
|
27
|
+
"lint": "eslint **/*.ts",
|
|
28
|
+
"lintfix": "eslint **/*.ts --fix",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"n8n": {
|
|
35
|
+
"n8nNodesApiVersion": 1,
|
|
36
|
+
"credentials": [
|
|
37
|
+
"dist/credentials/TwitterMediaUploadApi.credentials.js"
|
|
38
|
+
],
|
|
39
|
+
"nodes": [
|
|
40
|
+
"dist/nodes/TwitterMediaUpload/TwitterMediaUpload.node.js"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@typescript-eslint/parser": "^5.30.0",
|
|
45
|
+
"eslint": "^8.0.0",
|
|
46
|
+
"eslint-plugin-n8n-nodes-base": "^1.11.0",
|
|
47
|
+
"gulp": "^4.0.2",
|
|
48
|
+
"n8n-workflow": "*",
|
|
49
|
+
"prettier": "^2.7.1",
|
|
50
|
+
"typescript": "^4.8.0"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"oauth-1.0a": "^2.2.6",
|
|
54
|
+
"crypto-js": "^4.1.1"
|
|
55
|
+
}
|
|
56
|
+
}
|