@ldraney/mcp-linkedin 0.1.0
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/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +50 -0
- package/src/auth-server.js +223 -0
- package/src/database.js +288 -0
- package/src/index.js +654 -0
- package/src/linkedin-api.js +631 -0
- package/src/scheduler.js +201 -0
- package/src/schemas.js +629 -0
- package/src/tools.js +853 -0
- package/src/types.js +575 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file LinkedIn API client wrapper
|
|
3
|
+
* Handles all HTTP communication with LinkedIn REST API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const { URLSearchParams } = require('url');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Make an HTTPS request to LinkedIn API
|
|
11
|
+
* @param {Object} options - Request options
|
|
12
|
+
* @param {string} options.method - HTTP method
|
|
13
|
+
* @param {string} options.path - API path
|
|
14
|
+
* @param {Object} [options.headers] - HTTP headers
|
|
15
|
+
* @param {Object|string} [options.body] - Request body
|
|
16
|
+
* @returns {Promise<Object>} Response data and headers
|
|
17
|
+
*/
|
|
18
|
+
function makeRequest(options) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const { method, path, headers = {}, body } = options;
|
|
21
|
+
|
|
22
|
+
const requestOptions = {
|
|
23
|
+
hostname: 'api.linkedin.com',
|
|
24
|
+
path,
|
|
25
|
+
method,
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
...headers
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const req = https.request(requestOptions, (res) => {
|
|
33
|
+
let data = '';
|
|
34
|
+
|
|
35
|
+
res.on('data', (chunk) => {
|
|
36
|
+
data += chunk;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
const response = {
|
|
41
|
+
statusCode: res.statusCode,
|
|
42
|
+
headers: res.headers,
|
|
43
|
+
body: data ? (data.trim() ? JSON.parse(data) : {}) : {}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
47
|
+
resolve(response);
|
|
48
|
+
} else {
|
|
49
|
+
const error = new Error(`LinkedIn API error: ${res.statusCode}`);
|
|
50
|
+
error.response = response;
|
|
51
|
+
reject(error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
req.on('error', (error) => {
|
|
57
|
+
reject(error);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (body) {
|
|
61
|
+
const bodyString = typeof body === 'string' ? body : JSON.stringify(body);
|
|
62
|
+
req.write(bodyString);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
req.end();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Make an OAuth token request
|
|
71
|
+
* @param {string} hostname - OAuth hostname
|
|
72
|
+
* @param {string} path - OAuth path
|
|
73
|
+
* @param {Object} params - URL-encoded parameters
|
|
74
|
+
* @returns {Promise<Object>} Token response
|
|
75
|
+
*/
|
|
76
|
+
function makeOAuthRequest(hostname, path, params) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const postData = new URLSearchParams(params).toString();
|
|
79
|
+
|
|
80
|
+
const options = {
|
|
81
|
+
hostname,
|
|
82
|
+
path,
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
86
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const req = https.request(options, (res) => {
|
|
91
|
+
let data = '';
|
|
92
|
+
|
|
93
|
+
res.on('data', (chunk) => {
|
|
94
|
+
data += chunk;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
res.on('end', () => {
|
|
98
|
+
const response = JSON.parse(data);
|
|
99
|
+
|
|
100
|
+
if (res.statusCode === 200) {
|
|
101
|
+
resolve(response);
|
|
102
|
+
} else {
|
|
103
|
+
const error = new Error(response.error_description || 'OAuth error');
|
|
104
|
+
error.response = response;
|
|
105
|
+
reject(error);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
req.on('error', reject);
|
|
111
|
+
req.write(postData);
|
|
112
|
+
req.end();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* LinkedIn API client
|
|
118
|
+
*/
|
|
119
|
+
class LinkedInAPI {
|
|
120
|
+
/**
|
|
121
|
+
* @param {Object} config
|
|
122
|
+
* @param {string} config.accessToken - OAuth access token
|
|
123
|
+
* @param {string} config.apiVersion - API version (YYYYMM format)
|
|
124
|
+
* @param {string} config.personId - User's person URN
|
|
125
|
+
*/
|
|
126
|
+
constructor(config) {
|
|
127
|
+
this.accessToken = config.accessToken;
|
|
128
|
+
this.apiVersion = config.apiVersion;
|
|
129
|
+
this.personId = config.personId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create a LinkedIn post
|
|
134
|
+
* @param {Object} postData - Post data
|
|
135
|
+
* @returns {Promise<{postUrn: string, statusCode: number}>}
|
|
136
|
+
*/
|
|
137
|
+
async createPost(postData) {
|
|
138
|
+
const response = await makeRequest({
|
|
139
|
+
method: 'POST',
|
|
140
|
+
path: '/rest/posts',
|
|
141
|
+
headers: {
|
|
142
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
143
|
+
'LinkedIn-Version': this.apiVersion,
|
|
144
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
145
|
+
},
|
|
146
|
+
body: postData
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
postUrn: response.headers['x-restli-id'],
|
|
151
|
+
statusCode: response.statusCode
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get user's posts
|
|
157
|
+
* @param {Object} params
|
|
158
|
+
* @param {number} params.limit - Max posts to retrieve
|
|
159
|
+
* @param {number} params.offset - Pagination offset
|
|
160
|
+
* @returns {Promise<Object>} Posts list
|
|
161
|
+
*/
|
|
162
|
+
async getPosts({ limit, offset }) {
|
|
163
|
+
const authorUrn = encodeURIComponent(`urn:li:person:${this.personId}`);
|
|
164
|
+
const path = `/rest/posts?author=${authorUrn}&q=author&start=${offset}&count=${limit}`;
|
|
165
|
+
|
|
166
|
+
const response = await makeRequest({
|
|
167
|
+
method: 'GET',
|
|
168
|
+
path,
|
|
169
|
+
headers: {
|
|
170
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
171
|
+
'LinkedIn-Version': this.apiVersion,
|
|
172
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return response.body;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Delete a post
|
|
181
|
+
* @param {string} postUrn - Post URN to delete
|
|
182
|
+
* @returns {Promise<{statusCode: number}>}
|
|
183
|
+
*/
|
|
184
|
+
async deletePost(postUrn) {
|
|
185
|
+
const encodedUrn = encodeURIComponent(postUrn);
|
|
186
|
+
|
|
187
|
+
const response = await makeRequest({
|
|
188
|
+
method: 'DELETE',
|
|
189
|
+
path: `/rest/posts/${encodedUrn}`,
|
|
190
|
+
headers: {
|
|
191
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
192
|
+
'LinkedIn-Version': this.apiVersion,
|
|
193
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
statusCode: response.statusCode
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Update a post (partial update)
|
|
204
|
+
* @param {string} postUrn - Post URN to update
|
|
205
|
+
* @param {Object} updateData - Fields to update
|
|
206
|
+
* @param {string} [updateData.commentary] - New post text
|
|
207
|
+
* @param {string} [updateData.contentCallToActionLabel] - New CTA label
|
|
208
|
+
* @param {string} [updateData.contentLandingPage] - New landing page URL
|
|
209
|
+
* @returns {Promise<{statusCode: number}>}
|
|
210
|
+
*/
|
|
211
|
+
async updatePost(postUrn, updateData) {
|
|
212
|
+
const encodedUrn = encodeURIComponent(postUrn);
|
|
213
|
+
|
|
214
|
+
// Build the $set object with only provided fields
|
|
215
|
+
const $set = {};
|
|
216
|
+
if (updateData.commentary !== undefined) {
|
|
217
|
+
$set.commentary = updateData.commentary;
|
|
218
|
+
}
|
|
219
|
+
if (updateData.contentCallToActionLabel !== undefined) {
|
|
220
|
+
$set.contentCallToActionLabel = updateData.contentCallToActionLabel;
|
|
221
|
+
}
|
|
222
|
+
if (updateData.contentLandingPage !== undefined) {
|
|
223
|
+
$set.contentLandingPage = updateData.contentLandingPage;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const response = await makeRequest({
|
|
227
|
+
method: 'POST',
|
|
228
|
+
path: `/rest/posts/${encodedUrn}`,
|
|
229
|
+
headers: {
|
|
230
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
231
|
+
'LinkedIn-Version': this.apiVersion,
|
|
232
|
+
'X-Restli-Protocol-Version': '2.0.0',
|
|
233
|
+
'X-RestLi-Method': 'PARTIAL_UPDATE'
|
|
234
|
+
},
|
|
235
|
+
body: {
|
|
236
|
+
patch: { $set }
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
statusCode: response.statusCode
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Initialize image upload to get upload URL
|
|
247
|
+
* @returns {Promise<{uploadUrl: string, imageUrn: string}>}
|
|
248
|
+
*/
|
|
249
|
+
async initializeImageUpload() {
|
|
250
|
+
const response = await makeRequest({
|
|
251
|
+
method: 'POST',
|
|
252
|
+
path: '/rest/images?action=initializeUpload',
|
|
253
|
+
headers: {
|
|
254
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
255
|
+
'LinkedIn-Version': this.apiVersion,
|
|
256
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
257
|
+
},
|
|
258
|
+
body: {
|
|
259
|
+
initializeUploadRequest: {
|
|
260
|
+
owner: `urn:li:person:${this.personId}`
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
uploadUrl: response.body.value.uploadUrl,
|
|
267
|
+
imageUrn: response.body.value.image
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Upload image binary to LinkedIn
|
|
273
|
+
* @param {string} uploadUrl - URL from initializeImageUpload
|
|
274
|
+
* @param {Buffer} imageBuffer - Image binary data
|
|
275
|
+
* @param {string} contentType - MIME type (image/png, image/jpeg, image/gif)
|
|
276
|
+
* @returns {Promise<{statusCode: number}>}
|
|
277
|
+
*/
|
|
278
|
+
async uploadImageBinary(uploadUrl, imageBuffer, contentType) {
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
const url = new URL(uploadUrl);
|
|
281
|
+
|
|
282
|
+
const options = {
|
|
283
|
+
hostname: url.hostname,
|
|
284
|
+
path: url.pathname + url.search,
|
|
285
|
+
method: 'PUT',
|
|
286
|
+
headers: {
|
|
287
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
288
|
+
'Content-Type': contentType,
|
|
289
|
+
'Content-Length': imageBuffer.length
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const req = https.request(options, (res) => {
|
|
294
|
+
let data = '';
|
|
295
|
+
res.on('data', chunk => data += chunk);
|
|
296
|
+
res.on('end', () => {
|
|
297
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
298
|
+
resolve({ statusCode: res.statusCode });
|
|
299
|
+
} else {
|
|
300
|
+
const error = new Error(`Image upload failed: ${res.statusCode}`);
|
|
301
|
+
error.response = { statusCode: res.statusCode, body: data };
|
|
302
|
+
reject(error);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
req.on('error', reject);
|
|
308
|
+
req.write(imageBuffer);
|
|
309
|
+
req.end();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Refresh access token using refresh token
|
|
315
|
+
* @param {Object} params
|
|
316
|
+
* @param {string} params.refreshToken - Refresh token
|
|
317
|
+
* @param {string} params.clientId - OAuth client ID
|
|
318
|
+
* @param {string} params.clientSecret - OAuth client secret
|
|
319
|
+
* @returns {Promise<Object>} New token response
|
|
320
|
+
*/
|
|
321
|
+
static async refreshAccessToken({ refreshToken, clientId, clientSecret }) {
|
|
322
|
+
return makeOAuthRequest('www.linkedin.com', '/oauth/v2/accessToken', {
|
|
323
|
+
grant_type: 'refresh_token',
|
|
324
|
+
refresh_token: refreshToken,
|
|
325
|
+
client_id: clientId,
|
|
326
|
+
client_secret: clientSecret
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get user info from userinfo endpoint
|
|
332
|
+
* @returns {Promise<Object>} User information
|
|
333
|
+
*/
|
|
334
|
+
async getUserInfo() {
|
|
335
|
+
const response = await makeRequest({
|
|
336
|
+
method: 'GET',
|
|
337
|
+
path: '/v2/userinfo',
|
|
338
|
+
headers: {
|
|
339
|
+
'Authorization': `Bearer ${this.accessToken}`
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return response.body;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Exchange authorization code for access token
|
|
348
|
+
* @param {Object} params
|
|
349
|
+
* @param {string} params.code - Authorization code
|
|
350
|
+
* @param {string} params.clientId - OAuth client ID
|
|
351
|
+
* @param {string} params.clientSecret - OAuth client secret
|
|
352
|
+
* @param {string} params.redirectUri - OAuth redirect URI
|
|
353
|
+
* @returns {Promise<Object>} Token response
|
|
354
|
+
*/
|
|
355
|
+
static async exchangeAuthCode({ code, clientId, clientSecret, redirectUri }) {
|
|
356
|
+
return makeOAuthRequest('www.linkedin.com', '/oauth/v2/accessToken', {
|
|
357
|
+
grant_type: 'authorization_code',
|
|
358
|
+
code,
|
|
359
|
+
client_id: clientId,
|
|
360
|
+
client_secret: clientSecret,
|
|
361
|
+
redirect_uri: redirectUri
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Add a comment to a post
|
|
367
|
+
* @param {string} postUrn - Post URN to comment on
|
|
368
|
+
* @param {string} text - Comment text
|
|
369
|
+
* @returns {Promise<{commentUrn: string, statusCode: number}>}
|
|
370
|
+
*/
|
|
371
|
+
async addComment(postUrn, text) {
|
|
372
|
+
const encodedUrn = encodeURIComponent(postUrn);
|
|
373
|
+
|
|
374
|
+
const response = await makeRequest({
|
|
375
|
+
method: 'POST',
|
|
376
|
+
path: `/rest/socialActions/${encodedUrn}/comments`,
|
|
377
|
+
headers: {
|
|
378
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
379
|
+
'LinkedIn-Version': this.apiVersion,
|
|
380
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
381
|
+
},
|
|
382
|
+
body: {
|
|
383
|
+
actor: `urn:li:person:${this.personId}`,
|
|
384
|
+
message: {
|
|
385
|
+
text
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
commentUrn: response.headers['x-restli-id'],
|
|
392
|
+
statusCode: response.statusCode
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Add a reaction to a post
|
|
398
|
+
* @param {string} postUrn - Post URN to react to
|
|
399
|
+
* @param {string} reactionType - Type of reaction (LIKE, PRAISE, EMPATHY, INTEREST, APPRECIATION, ENTERTAINMENT)
|
|
400
|
+
* @returns {Promise<{statusCode: number}>}
|
|
401
|
+
*/
|
|
402
|
+
async addReaction(postUrn, reactionType) {
|
|
403
|
+
const actorUrn = encodeURIComponent(`urn:li:person:${this.personId}`);
|
|
404
|
+
|
|
405
|
+
const response = await makeRequest({
|
|
406
|
+
method: 'POST',
|
|
407
|
+
path: `/rest/reactions?actor=${actorUrn}`,
|
|
408
|
+
headers: {
|
|
409
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
410
|
+
'LinkedIn-Version': this.apiVersion,
|
|
411
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
412
|
+
},
|
|
413
|
+
body: {
|
|
414
|
+
root: postUrn,
|
|
415
|
+
reactionType
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
statusCode: response.statusCode
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Initialize document upload to get upload URL
|
|
426
|
+
* @returns {Promise<{uploadUrl: string, documentUrn: string}>}
|
|
427
|
+
*/
|
|
428
|
+
async initializeDocumentUpload() {
|
|
429
|
+
const response = await makeRequest({
|
|
430
|
+
method: 'POST',
|
|
431
|
+
path: '/rest/documents?action=initializeUpload',
|
|
432
|
+
headers: {
|
|
433
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
434
|
+
'LinkedIn-Version': this.apiVersion,
|
|
435
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
436
|
+
},
|
|
437
|
+
body: {
|
|
438
|
+
initializeUploadRequest: {
|
|
439
|
+
owner: `urn:li:person:${this.personId}`
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
uploadUrl: response.body.value.uploadUrl,
|
|
446
|
+
documentUrn: response.body.value.document
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Upload document binary to LinkedIn
|
|
452
|
+
* @param {string} uploadUrl - URL from initializeDocumentUpload
|
|
453
|
+
* @param {Buffer} documentBuffer - Document binary data
|
|
454
|
+
* @param {string} contentType - MIME type
|
|
455
|
+
* @returns {Promise<{statusCode: number}>}
|
|
456
|
+
*/
|
|
457
|
+
async uploadDocumentBinary(uploadUrl, documentBuffer, contentType) {
|
|
458
|
+
return new Promise((resolve, reject) => {
|
|
459
|
+
const url = new URL(uploadUrl);
|
|
460
|
+
|
|
461
|
+
const options = {
|
|
462
|
+
hostname: url.hostname,
|
|
463
|
+
path: url.pathname + url.search,
|
|
464
|
+
method: 'PUT',
|
|
465
|
+
headers: {
|
|
466
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
467
|
+
'Content-Type': contentType,
|
|
468
|
+
'Content-Length': documentBuffer.length
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const req = https.request(options, (res) => {
|
|
473
|
+
let data = '';
|
|
474
|
+
res.on('data', chunk => data += chunk);
|
|
475
|
+
res.on('end', () => {
|
|
476
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
477
|
+
resolve({ statusCode: res.statusCode });
|
|
478
|
+
} else {
|
|
479
|
+
const error = new Error(`Document upload failed: ${res.statusCode}`);
|
|
480
|
+
error.response = { statusCode: res.statusCode, body: data };
|
|
481
|
+
reject(error);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
req.on('error', reject);
|
|
487
|
+
req.write(documentBuffer);
|
|
488
|
+
req.end();
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Initialize video upload to get upload URL and video URN
|
|
494
|
+
* @param {number} fileSizeBytes - Size of video file in bytes
|
|
495
|
+
* @returns {Promise<{uploadUrl: string, videoUrn: string}>}
|
|
496
|
+
*/
|
|
497
|
+
async initializeVideoUpload(fileSizeBytes) {
|
|
498
|
+
const response = await makeRequest({
|
|
499
|
+
method: 'POST',
|
|
500
|
+
path: '/rest/videos?action=initializeUpload',
|
|
501
|
+
headers: {
|
|
502
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
503
|
+
'LinkedIn-Version': this.apiVersion,
|
|
504
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
505
|
+
},
|
|
506
|
+
body: {
|
|
507
|
+
initializeUploadRequest: {
|
|
508
|
+
owner: `urn:li:person:${this.personId}`,
|
|
509
|
+
fileSizeBytes,
|
|
510
|
+
uploadCaptions: false,
|
|
511
|
+
uploadThumbnail: false
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
uploadUrl: response.body.value.uploadInstructions[0].uploadUrl,
|
|
518
|
+
videoUrn: response.body.value.video
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Upload video binary to LinkedIn
|
|
524
|
+
* @param {string} uploadUrl - URL from initializeVideoUpload
|
|
525
|
+
* @param {Buffer} videoBuffer - Video binary data
|
|
526
|
+
* @param {string} contentType - MIME type
|
|
527
|
+
* @returns {Promise<{statusCode: number, etag: string}>}
|
|
528
|
+
*/
|
|
529
|
+
async uploadVideoBinary(uploadUrl, videoBuffer, contentType) {
|
|
530
|
+
return new Promise((resolve, reject) => {
|
|
531
|
+
const url = new URL(uploadUrl);
|
|
532
|
+
|
|
533
|
+
const options = {
|
|
534
|
+
hostname: url.hostname,
|
|
535
|
+
path: url.pathname + url.search,
|
|
536
|
+
method: 'PUT',
|
|
537
|
+
headers: {
|
|
538
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
539
|
+
'Content-Type': contentType,
|
|
540
|
+
'Content-Length': videoBuffer.length
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const req = https.request(options, (res) => {
|
|
545
|
+
let data = '';
|
|
546
|
+
res.on('data', chunk => data += chunk);
|
|
547
|
+
res.on('end', () => {
|
|
548
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
549
|
+
resolve({
|
|
550
|
+
statusCode: res.statusCode,
|
|
551
|
+
etag: res.headers['etag'] || ''
|
|
552
|
+
});
|
|
553
|
+
} else {
|
|
554
|
+
const error = new Error(`Video upload failed: ${res.statusCode}`);
|
|
555
|
+
error.response = { statusCode: res.statusCode, body: data };
|
|
556
|
+
reject(error);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
req.on('error', reject);
|
|
562
|
+
req.write(videoBuffer);
|
|
563
|
+
req.end();
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Finalize video upload after binary upload is complete
|
|
569
|
+
* @param {string} videoUrn - Video URN from initializeVideoUpload
|
|
570
|
+
* @param {string} uploadUrl - Upload URL used
|
|
571
|
+
* @param {string} etag - ETag from upload response
|
|
572
|
+
* @returns {Promise<{statusCode: number}>}
|
|
573
|
+
*/
|
|
574
|
+
async finalizeVideoUpload(videoUrn, uploadUrl, etag) {
|
|
575
|
+
const response = await makeRequest({
|
|
576
|
+
method: 'POST',
|
|
577
|
+
path: '/rest/videos?action=finalizeUpload',
|
|
578
|
+
headers: {
|
|
579
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
580
|
+
'LinkedIn-Version': this.apiVersion,
|
|
581
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
582
|
+
},
|
|
583
|
+
body: {
|
|
584
|
+
finalizeUploadRequest: {
|
|
585
|
+
video: videoUrn,
|
|
586
|
+
uploadToken: '',
|
|
587
|
+
uploadedPartIds: [etag]
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
statusCode: response.statusCode
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Initialize multi-image upload for batch image upload
|
|
599
|
+
* @param {number} imageCount - Number of images to upload
|
|
600
|
+
* @returns {Promise<Array<{uploadUrl: string, imageUrn: string}>>}
|
|
601
|
+
*/
|
|
602
|
+
async initializeMultiImageUpload(imageCount) {
|
|
603
|
+
const results = [];
|
|
604
|
+
|
|
605
|
+
for (let i = 0; i < imageCount; i++) {
|
|
606
|
+
const response = await makeRequest({
|
|
607
|
+
method: 'POST',
|
|
608
|
+
path: '/rest/images?action=initializeUpload',
|
|
609
|
+
headers: {
|
|
610
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
611
|
+
'LinkedIn-Version': this.apiVersion,
|
|
612
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
613
|
+
},
|
|
614
|
+
body: {
|
|
615
|
+
initializeUploadRequest: {
|
|
616
|
+
owner: `urn:li:person:${this.personId}`
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
results.push({
|
|
622
|
+
uploadUrl: response.body.value.uploadUrl,
|
|
623
|
+
imageUrn: response.body.value.image
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return results;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
module.exports = LinkedInAPI;
|