@opencoredev/social-sdk 0.1.1 → 0.2.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/README.md +10 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud/common.d.ts.map +1 -1
- package/dist/cloud/common.js +48 -20
- package/dist/cloud/common.js.map +1 -1
- package/dist/cloud/lifecycle.d.ts.map +1 -1
- package/dist/cloud/lifecycle.js +5 -0
- package/dist/cloud/lifecycle.js.map +1 -1
- package/dist/cloud/media.d.ts.map +1 -1
- package/dist/cloud/media.js +12 -8
- package/dist/cloud/media.js.map +1 -1
- package/dist/cloud/outcomes.d.ts.map +1 -1
- package/dist/cloud/outcomes.js +17 -9
- package/dist/cloud/outcomes.js.map +1 -1
- package/dist/cloud/post-for-me.d.ts.map +1 -1
- package/dist/cloud/post-for-me.js +99 -28
- package/dist/cloud/post-for-me.js.map +1 -1
- package/dist/cloud/zernio.d.ts.map +1 -1
- package/dist/cloud/zernio.js +100 -9
- package/dist/cloud/zernio.js.map +1 -1
- package/dist/core/adapter.d.ts +42 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/client.d.ts +35 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +503 -110
- package/dist/core/client.js.map +1 -1
- package/dist/core/concurrency.d.ts.map +1 -1
- package/dist/core/concurrency.js +5 -3
- package/dist/core/concurrency.js.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +14 -7
- package/dist/core/errors.js.map +1 -1
- package/dist/core/idempotency.d.ts.map +1 -1
- package/dist/core/idempotency.js.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/pagination.d.ts.map +1 -1
- package/dist/core/pagination.js +1 -0
- package/dist/core/pagination.js.map +1 -1
- package/dist/core/types.d.ts +70 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -1
- package/dist/platforms/bluesky.d.ts +131 -1
- package/dist/platforms/bluesky.d.ts.map +1 -1
- package/dist/platforms/bluesky.js +772 -44
- package/dist/platforms/bluesky.js.map +1 -1
- package/dist/platforms/instagram.d.ts +70 -3
- package/dist/platforms/instagram.d.ts.map +1 -1
- package/dist/platforms/instagram.js +403 -47
- package/dist/platforms/instagram.js.map +1 -1
- package/dist/platforms/linkedin.d.ts +55 -0
- package/dist/platforms/linkedin.d.ts.map +1 -1
- package/dist/platforms/linkedin.js +350 -39
- package/dist/platforms/linkedin.js.map +1 -1
- package/dist/platforms/threads.d.ts +37 -4
- package/dist/platforms/threads.d.ts.map +1 -1
- package/dist/platforms/threads.js +419 -44
- package/dist/platforms/threads.js.map +1 -1
- package/dist/platforms/tiktok.d.ts.map +1 -1
- package/dist/platforms/tiktok.js +109 -26
- package/dist/platforms/tiktok.js.map +1 -1
- package/dist/platforms/x-engagement.d.ts.map +1 -1
- package/dist/platforms/x-engagement.js +4 -0
- package/dist/platforms/x-engagement.js.map +1 -1
- package/dist/platforms/x.d.ts +227 -9
- package/dist/platforms/x.d.ts.map +1 -1
- package/dist/platforms/x.js +826 -49
- package/dist/platforms/x.js.map +1 -1
- package/dist/platforms/youtube-upload.d.ts.map +1 -1
- package/dist/platforms/youtube-upload.js +25 -9
- package/dist/platforms/youtube-upload.js.map +1 -1
- package/dist/platforms/youtube.d.ts +56 -2
- package/dist/platforms/youtube.d.ts.map +1 -1
- package/dist/platforms/youtube.js +571 -31
- package/dist/platforms/youtube.js.map +1 -1
- package/dist/server/connections.d.ts.map +1 -1
- package/dist/server/connections.js +4 -2
- package/dist/server/connections.js.map +1 -1
- package/dist/server/credentials.d.ts.map +1 -1
- package/dist/server/credentials.js +10 -16
- package/dist/server/credentials.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth.d.ts +5 -0
- package/dist/server/oauth.d.ts.map +1 -1
- package/dist/server/oauth.js +118 -47
- package/dist/server/oauth.js.map +1 -1
- package/dist/server/webhooks.d.ts.map +1 -1
- package/dist/server/webhooks.js +11 -4
- package/dist/server/webhooks.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +153 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/transport/binary.d.ts.map +1 -1
- package/dist/transport/binary.js.map +1 -1
- package/dist/transport/budget.d.ts.map +1 -1
- package/dist/transport/budget.js.map +1 -1
- package/dist/transport/http.d.ts +2 -1
- package/dist/transport/http.d.ts.map +1 -1
- package/dist/transport/http.js +42 -12
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/json.d.ts +7 -1
- package/dist/transport/json.d.ts.map +1 -1
- package/dist/transport/json.js +7 -1
- package/dist/transport/json.js.map +1 -1
- package/dist/transport/upload.d.ts +1 -0
- package/dist/transport/upload.d.ts.map +1 -1
- package/dist/transport/upload.js +38 -17
- package/dist/transport/upload.js.map +1 -1
- package/dist/transport/validation.d.ts.map +1 -1
- package/dist/transport/validation.js +1 -0
- package/dist/transport/validation.js.map +1 -1
- package/package.json +2 -1
|
@@ -17,6 +17,7 @@ export function linkedin(options) {
|
|
|
17
17
|
}
|
|
18
18
|
const http = createHttp(options.fetch ? { fetch: options.fetch } : {});
|
|
19
19
|
const now = () => (options.clock?.() ?? new Date()).toISOString();
|
|
20
|
+
const escapeCommentary = (text) => text.replace(/[|{}@()[\]<>#\\*_~]/g, "\\$&");
|
|
20
21
|
const authorize = (ref, context) => {
|
|
21
22
|
if (ref.backend !== context.backendInstance ||
|
|
22
23
|
ref.platform !== "linkedin" ||
|
|
@@ -27,7 +28,7 @@ export function linkedin(options) {
|
|
|
27
28
|
message: "Reference does not belong to this LinkedIn author authorization.",
|
|
28
29
|
});
|
|
29
30
|
};
|
|
30
|
-
async function request(path, context, body, selectedHeaders, method = body === undefined ? "GET" : "POST") {
|
|
31
|
+
async function request(path, context, body, selectedHeaders, method = body === undefined ? "GET" : "POST", extraHeaders) {
|
|
31
32
|
try {
|
|
32
33
|
return await http({
|
|
33
34
|
timeoutMs: remainingBudget(context),
|
|
@@ -37,10 +38,14 @@ export function linkedin(options) {
|
|
|
37
38
|
"Content-Type": "application/json",
|
|
38
39
|
"Linkedin-Version": options.apiVersion,
|
|
39
40
|
"X-Restli-Protocol-Version": "2.0.0",
|
|
41
|
+
...extraHeaders,
|
|
40
42
|
},
|
|
41
43
|
method,
|
|
44
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
42
45
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
46
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
43
47
|
...(context.signal ? { signal: context.signal } : {}),
|
|
48
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
44
49
|
...(selectedHeaders ? { responseHeaders: selectedHeaders } : {}),
|
|
45
50
|
maxAttempts: method === "GET" ? Math.min(5, context.retryBudget.maxAttempts) : 1,
|
|
46
51
|
});
|
|
@@ -54,17 +59,27 @@ export function linkedin(options) {
|
|
|
54
59
|
throw new SocialError({
|
|
55
60
|
code: ambiguous
|
|
56
61
|
? "ambiguous_outcome"
|
|
57
|
-
: error.
|
|
58
|
-
? "
|
|
59
|
-
: error.
|
|
60
|
-
? "
|
|
61
|
-
: error.status ===
|
|
62
|
-
? "
|
|
63
|
-
:
|
|
62
|
+
: error.kind === "timeout"
|
|
63
|
+
? "timeout"
|
|
64
|
+
: error.kind === "cancelled"
|
|
65
|
+
? "cancelled"
|
|
66
|
+
: error.status === 401
|
|
67
|
+
? "reconnect_required"
|
|
68
|
+
: error.status === 403
|
|
69
|
+
? "missing_permission"
|
|
70
|
+
: error.status === 404
|
|
71
|
+
? "not_found"
|
|
72
|
+
: error.status === 429
|
|
73
|
+
? "rate_limited"
|
|
74
|
+
: "upstream_failure",
|
|
64
75
|
operation: path,
|
|
65
76
|
message: error.message,
|
|
66
77
|
upstreamStatus: error.status,
|
|
67
|
-
retryDisposition: ambiguous
|
|
78
|
+
retryDisposition: ambiguous
|
|
79
|
+
? { kind: "reconcile-first" }
|
|
80
|
+
: error.status === 429 && error.retryAfterMs !== undefined
|
|
81
|
+
? { kind: "after-delay", delayMs: error.retryAfterMs }
|
|
82
|
+
: { kind: "never" },
|
|
68
83
|
});
|
|
69
84
|
}
|
|
70
85
|
}
|
|
@@ -116,12 +131,15 @@ export function linkedin(options) {
|
|
|
116
131
|
url: string(initialized["uploadUrl"]),
|
|
117
132
|
source: {
|
|
118
133
|
mimeType: media.mimeType,
|
|
134
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
119
135
|
...(size === undefined ? {} : { size }),
|
|
120
136
|
open: source.kind === "blob" ? () => source.blob.stream() : source.open,
|
|
121
137
|
},
|
|
122
138
|
allowHost: (host) => host === "www.linkedin.com",
|
|
123
139
|
maxBytes: 20 * 1024 * 1024,
|
|
140
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
124
141
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
142
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
125
143
|
...(context.signal ? { signal: context.signal } : {}),
|
|
126
144
|
});
|
|
127
145
|
return {
|
|
@@ -133,6 +151,208 @@ export function linkedin(options) {
|
|
|
133
151
|
mediaId,
|
|
134
152
|
};
|
|
135
153
|
}
|
|
154
|
+
/* oxlint-disable anti-slop/no-unknown-parameters, anti-slop/no-unsafe-dictionary-type, anti-slop/no-known-value-widening, anti-slop/no-conditional-empty-object-spread, anti-slop/no-runtime-typeof, anti-slop/require-readable-spacing -- These helpers normalize documented LinkedIn response facets at the transport boundary. */
|
|
155
|
+
const organizationOnly = (account, context, operation) => {
|
|
156
|
+
authorize(account, context);
|
|
157
|
+
if (!account.accountId.startsWith("urn:li:organization:"))
|
|
158
|
+
throw new SocialError({
|
|
159
|
+
code: "unsupported_capability",
|
|
160
|
+
operation,
|
|
161
|
+
message: "LinkedIn organization analytics require an organization account, not a member account.",
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
const statisticsPath = (path, finder, account, interval) => {
|
|
165
|
+
const params = [`q=${finder}`, `${finder}=${encodeURIComponent(account.accountId)}`];
|
|
166
|
+
if (interval) {
|
|
167
|
+
if (interval.start !== undefined &&
|
|
168
|
+
(!Number.isSafeInteger(interval.start) || interval.start < 0))
|
|
169
|
+
throw new SocialError({
|
|
170
|
+
code: "invalid_input",
|
|
171
|
+
operation: "analytics.organization.read",
|
|
172
|
+
message: "Interval start must be a nonnegative epoch-millisecond integer.",
|
|
173
|
+
});
|
|
174
|
+
if (interval.end !== undefined && (!Number.isSafeInteger(interval.end) || interval.end < 0))
|
|
175
|
+
throw new SocialError({
|
|
176
|
+
code: "invalid_input",
|
|
177
|
+
operation: "analytics.organization.read",
|
|
178
|
+
message: "Interval end must be a nonnegative epoch-millisecond integer.",
|
|
179
|
+
});
|
|
180
|
+
if (interval.start !== undefined &&
|
|
181
|
+
interval.end !== undefined &&
|
|
182
|
+
interval.end <= interval.start)
|
|
183
|
+
throw new SocialError({
|
|
184
|
+
code: "invalid_input",
|
|
185
|
+
operation: "analytics.organization.read",
|
|
186
|
+
message: "Interval end must be after interval start.",
|
|
187
|
+
});
|
|
188
|
+
if (interval.start === undefined)
|
|
189
|
+
throw new SocialError({
|
|
190
|
+
code: "invalid_input",
|
|
191
|
+
operation: "analytics.organization.read",
|
|
192
|
+
message: "Interval start is required for time-bound organization statistics.",
|
|
193
|
+
});
|
|
194
|
+
const rangeParts = [
|
|
195
|
+
`start:${interval.start}`,
|
|
196
|
+
...(interval.end === undefined ? [] : [`end:${interval.end}`]),
|
|
197
|
+
];
|
|
198
|
+
const range = `(timeRange:(${rangeParts.join(",")}),timeGranularityType:${interval.granularity})`;
|
|
199
|
+
if (!["DAY", "WEEK", "MONTH"].includes(interval.granularity))
|
|
200
|
+
throw new SocialError({
|
|
201
|
+
code: "invalid_input",
|
|
202
|
+
operation: "analytics.organization.read",
|
|
203
|
+
message: "Interval granularity must be DAY, WEEK or MONTH.",
|
|
204
|
+
});
|
|
205
|
+
params.push(`timeIntervals=${range}`);
|
|
206
|
+
}
|
|
207
|
+
return `${path}?${params.join("&")}`;
|
|
208
|
+
};
|
|
209
|
+
const numberMap = (value) => {
|
|
210
|
+
const row = value === undefined ? {} : object(value);
|
|
211
|
+
const output = {};
|
|
212
|
+
for (const [key, item] of Object.entries(row)) {
|
|
213
|
+
const number = optionalNumber(item);
|
|
214
|
+
if (number !== undefined)
|
|
215
|
+
output[key] = number;
|
|
216
|
+
else if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
217
|
+
const nestedObject = object(item);
|
|
218
|
+
const nested = ["pageViews", "uniquePageViews", "clicks", "count"]
|
|
219
|
+
.map((name) => optionalNumber(nestedObject[name]))
|
|
220
|
+
.find((candidate) => candidate !== undefined);
|
|
221
|
+
if (nested !== undefined)
|
|
222
|
+
output[key] = nested;
|
|
223
|
+
else {
|
|
224
|
+
for (const [nestedKey, nestedValue] of Object.entries(nestedObject)) {
|
|
225
|
+
const nestedNumber = optionalNumber(nestedValue);
|
|
226
|
+
if (nestedNumber !== undefined)
|
|
227
|
+
output[`${key}.${nestedKey}`] = nestedNumber;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return output;
|
|
233
|
+
};
|
|
234
|
+
const followerBreakdowns = (row) => {
|
|
235
|
+
const dimensions = [
|
|
236
|
+
["function", "followerCountsByFunction", "function"],
|
|
237
|
+
["seniority", "followerCountsBySeniority", "seniority"],
|
|
238
|
+
["industry", "followerCountsByIndustry", "industry"],
|
|
239
|
+
["geo", "followerCountsByGeo", "geo"],
|
|
240
|
+
["geoCountry", "followerCountsByGeoCountry", "geo"],
|
|
241
|
+
["staffCountRange", "followerCountsByStaffCountRange", "staffCountRange"],
|
|
242
|
+
["associationType", "followerCountsByAssociationType", "associationType"],
|
|
243
|
+
];
|
|
244
|
+
const output = [];
|
|
245
|
+
for (const [dimension, field, key] of dimensions) {
|
|
246
|
+
if (row[field] === undefined)
|
|
247
|
+
continue;
|
|
248
|
+
for (const value of array(row[field])) {
|
|
249
|
+
const item = object(value);
|
|
250
|
+
const label = optionalString(item[key]);
|
|
251
|
+
if (!label)
|
|
252
|
+
continue;
|
|
253
|
+
const counts = item["followerCounts"] === undefined ? {} : object(item["followerCounts"]);
|
|
254
|
+
output.push({
|
|
255
|
+
dimension,
|
|
256
|
+
value: label,
|
|
257
|
+
...(optionalNumber(counts["organicFollowerCount"]) === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: { organicFollowerCount: optionalNumber(counts["organicFollowerCount"]) }),
|
|
260
|
+
...(optionalNumber(counts["paidFollowerCount"]) === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: { paidFollowerCount: optionalNumber(counts["paidFollowerCount"]) }),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return output;
|
|
267
|
+
};
|
|
268
|
+
const parseInterval = (row, requestedGranularity) => {
|
|
269
|
+
if (row["timeRange"] === undefined)
|
|
270
|
+
return undefined;
|
|
271
|
+
const range = object(row["timeRange"]);
|
|
272
|
+
const start = optionalNumber(range["start"]);
|
|
273
|
+
const end = optionalNumber(range["end"]);
|
|
274
|
+
const granularity = requestedGranularity ?? optionalString(row["timeGranularityType"]);
|
|
275
|
+
if ((granularity !== "DAY" && granularity !== "WEEK" && granularity !== "MONTH") ||
|
|
276
|
+
(start === undefined && end === undefined))
|
|
277
|
+
return undefined;
|
|
278
|
+
return {
|
|
279
|
+
granularity,
|
|
280
|
+
...(start === undefined ? {} : { start }),
|
|
281
|
+
...(end === undefined ? {} : { end }),
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
const parseFollowerStatistics = (result, requestedGranularity) => array(result["elements"]).map((value) => {
|
|
285
|
+
const row = object(value);
|
|
286
|
+
const gains = row["followerGains"] === undefined ? {} : object(row["followerGains"]);
|
|
287
|
+
return {
|
|
288
|
+
organization: string(row["organizationalEntity"]),
|
|
289
|
+
...(parseInterval(row, requestedGranularity) === undefined
|
|
290
|
+
? {}
|
|
291
|
+
: { interval: parseInterval(row, requestedGranularity) }),
|
|
292
|
+
...(optionalNumber(gains["organicFollowerGain"]) === undefined
|
|
293
|
+
? {}
|
|
294
|
+
: { organicFollowerGain: optionalNumber(gains["organicFollowerGain"]) }),
|
|
295
|
+
...(optionalNumber(gains["paidFollowerGain"]) === undefined
|
|
296
|
+
? {}
|
|
297
|
+
: { paidFollowerGain: optionalNumber(gains["paidFollowerGain"]) }),
|
|
298
|
+
breakdowns: followerBreakdowns(row),
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
const pageBreakdowns = (row) => {
|
|
302
|
+
const output = [];
|
|
303
|
+
for (const [field, dimension, key] of [
|
|
304
|
+
["pageStatisticsByFunction", "function", "function"],
|
|
305
|
+
["pageStatisticsBySeniority", "seniority", "seniority"],
|
|
306
|
+
["pageStatisticsByIndustryV2", "industryV2", "industryV2"],
|
|
307
|
+
["pageStatisticsByIndustry", "industry", "industry"],
|
|
308
|
+
["pageStatisticsByGeo", "geo", "geo"],
|
|
309
|
+
["pageStatisticsByGeoCountry", "geoCountry", "geo"],
|
|
310
|
+
["pageStatisticsByStaffCountRange", "staffCountRange", "staffCountRange"],
|
|
311
|
+
]) {
|
|
312
|
+
if (row[field] === undefined)
|
|
313
|
+
continue;
|
|
314
|
+
for (const value of array(row[field])) {
|
|
315
|
+
const item = object(value);
|
|
316
|
+
const label = optionalString(item[key]);
|
|
317
|
+
if (!label)
|
|
318
|
+
continue;
|
|
319
|
+
const stats = item["pageStatistics"] === undefined ? {} : object(item["pageStatistics"]);
|
|
320
|
+
const views = stats["views"] === undefined ? {} : object(stats["views"]);
|
|
321
|
+
const clicks = stats["clicks"] === undefined ? {} : object(stats["clicks"]);
|
|
322
|
+
output.push({
|
|
323
|
+
dimension,
|
|
324
|
+
value: label,
|
|
325
|
+
views: numberMap(views),
|
|
326
|
+
clicks: numberMap(clicks),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return output;
|
|
331
|
+
};
|
|
332
|
+
const parsePageStatistics = (result, requestedGranularity) => array(result["elements"]).map((value) => {
|
|
333
|
+
const row = object(value);
|
|
334
|
+
const total = row["totalPageStatistics"] === undefined ? {} : object(row["totalPageStatistics"]);
|
|
335
|
+
return {
|
|
336
|
+
organization: string(row["organization"]),
|
|
337
|
+
...(parseInterval(row, requestedGranularity) === undefined
|
|
338
|
+
? {}
|
|
339
|
+
: { interval: parseInterval(row, requestedGranularity) }),
|
|
340
|
+
views: numberMap(total["views"]),
|
|
341
|
+
clicks: numberMap(total["clicks"]),
|
|
342
|
+
breakdowns: pageBreakdowns(row),
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
const parseShareStatistics = (result, requestedGranularity) => array(result["elements"]).map((value) => {
|
|
346
|
+
const row = object(value);
|
|
347
|
+
return {
|
|
348
|
+
organization: string(row["organizationalEntity"]),
|
|
349
|
+
...(parseInterval(row, requestedGranularity) === undefined
|
|
350
|
+
? {}
|
|
351
|
+
: { interval: parseInterval(row, requestedGranularity) }),
|
|
352
|
+
metrics: numberMap(row["totalShareStatistics"]),
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
/* oxlint-enable anti-slop/no-unknown-parameters, anti-slop/no-unsafe-dictionary-type, anti-slop/no-known-value-widening, anti-slop/no-conditional-empty-object-spread, anti-slop/no-runtime-typeof */
|
|
136
356
|
return defineAdapter({
|
|
137
357
|
id: "linkedin",
|
|
138
358
|
capabilities: {
|
|
@@ -167,25 +387,38 @@ export function linkedin(options) {
|
|
|
167
387
|
{
|
|
168
388
|
platform: "linkedin",
|
|
169
389
|
operation: "posts.multi-image",
|
|
170
|
-
availability: "
|
|
390
|
+
availability: "not-implemented-by-adapter",
|
|
171
391
|
formats: ["carousel"],
|
|
172
392
|
},
|
|
173
393
|
{
|
|
174
394
|
platform: "linkedin",
|
|
175
395
|
operation: "posts.video",
|
|
176
|
-
availability: "
|
|
396
|
+
availability: "not-implemented-by-adapter",
|
|
177
397
|
formats: ["video"],
|
|
178
398
|
},
|
|
179
|
-
{
|
|
399
|
+
{
|
|
400
|
+
platform: "linkedin",
|
|
401
|
+
operation: "posts.document",
|
|
402
|
+
availability: "not-implemented-by-adapter",
|
|
403
|
+
},
|
|
180
404
|
{ platform: "linkedin", operation: "polls.create", availability: "available" },
|
|
181
405
|
{ platform: "linkedin", operation: "reactions.write", availability: "available" },
|
|
182
406
|
{ platform: "linkedin", operation: "reshares.write", availability: "available" },
|
|
183
407
|
{ platform: "linkedin", operation: "posts.update", availability: "available" },
|
|
184
408
|
{ platform: "linkedin", operation: "posts.delete", availability: "available" },
|
|
409
|
+
{
|
|
410
|
+
platform: "linkedin",
|
|
411
|
+
operation: "posts.removeFromPlatform",
|
|
412
|
+
availability: "available",
|
|
413
|
+
},
|
|
185
414
|
{
|
|
186
415
|
platform: "linkedin",
|
|
187
416
|
operation: "analytics.organization.read",
|
|
188
|
-
availability: "
|
|
417
|
+
availability: options.auth.author.startsWith("urn:li:organization:")
|
|
418
|
+
? "available"
|
|
419
|
+
: "account-ineligible",
|
|
420
|
+
requiredScopes: ["rw_organization_admin"],
|
|
421
|
+
notes: "Requires the Community Management API product and organization administrator access. Member accounts are not eligible.",
|
|
189
422
|
},
|
|
190
423
|
{
|
|
191
424
|
platform: "linkedin",
|
|
@@ -206,6 +439,15 @@ export function linkedin(options) {
|
|
|
206
439
|
requiredScopes: ["rw_organization_admin"],
|
|
207
440
|
notes: "Organization total followers only; authenticated member must administer the organization. No member-profile analytics claimed.",
|
|
208
441
|
},
|
|
442
|
+
...["analytics.followers.read", "analytics.page.read", "analytics.shares.read"].map((operation) => ({
|
|
443
|
+
platform: "linkedin",
|
|
444
|
+
operation,
|
|
445
|
+
availability: options.auth.author.startsWith("urn:li:organization:")
|
|
446
|
+
? "available"
|
|
447
|
+
: "account-ineligible",
|
|
448
|
+
requiredScopes: ["rw_organization_admin"],
|
|
449
|
+
notes: "Community Management API product and organization administrator access are required. Member accounts are not eligible.",
|
|
450
|
+
})),
|
|
209
451
|
{
|
|
210
452
|
platform: "linkedin",
|
|
211
453
|
operation: "posts.list",
|
|
@@ -215,7 +457,7 @@ export function linkedin(options) {
|
|
|
215
457
|
? "r_organization_social"
|
|
216
458
|
: "r_member_social",
|
|
217
459
|
],
|
|
218
|
-
notes: "Author
|
|
460
|
+
notes: "Author-feed pagination uses the returned offset cursor.",
|
|
219
461
|
},
|
|
220
462
|
{
|
|
221
463
|
platform: "linkedin",
|
|
@@ -227,6 +469,9 @@ export function linkedin(options) {
|
|
|
227
469
|
platform: "linkedin",
|
|
228
470
|
operation,
|
|
229
471
|
availability: "available",
|
|
472
|
+
...(operation === "analytics.read"
|
|
473
|
+
? { requiredScopes: ["r_member_social"] }
|
|
474
|
+
: { requiredScopes: ["w_member_social", "r_member_social"] }),
|
|
230
475
|
notes: "Community Management product permissions and author post read access required. Analytics contains returned social-action counts only.",
|
|
231
476
|
})),
|
|
232
477
|
],
|
|
@@ -269,14 +514,24 @@ export function linkedin(options) {
|
|
|
269
514
|
let content;
|
|
270
515
|
if (media?.source.kind === "media-ref") {
|
|
271
516
|
authorize(media.source.ref, context);
|
|
272
|
-
|
|
273
|
-
|
|
517
|
+
let image;
|
|
518
|
+
try {
|
|
519
|
+
// SAFETY: object() validates the upstream response as a JSON object.
|
|
520
|
+
image = object(await request(`/rest/images/${encodeURIComponent(media.source.ref.mediaId)}`, context));
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
if (!(error instanceof SocialError) ||
|
|
524
|
+
error.code !== "missing_permission" ||
|
|
525
|
+
!options.auth.author.startsWith("urn:li:person:"))
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
if (image?.["owner"] !== undefined && image["owner"] !== target.account.accountId)
|
|
274
529
|
throw new SocialError({
|
|
275
530
|
code: "unauthorized",
|
|
276
531
|
operation: "posts.publish",
|
|
277
532
|
message: "LinkedIn image belongs to a different author.",
|
|
278
533
|
});
|
|
279
|
-
if (image["status"] !== "AVAILABLE")
|
|
534
|
+
if (image?.["status"] !== undefined && image["status"] !== "AVAILABLE")
|
|
280
535
|
throw new SocialError({
|
|
281
536
|
code: "media_error",
|
|
282
537
|
operation: "posts.publish",
|
|
@@ -285,13 +540,14 @@ export function linkedin(options) {
|
|
|
285
540
|
content = {
|
|
286
541
|
media: {
|
|
287
542
|
id: media.source.ref.mediaId,
|
|
543
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
288
544
|
...(media.altText ? { altText: media.altText } : {}),
|
|
289
545
|
},
|
|
290
546
|
};
|
|
291
547
|
}
|
|
292
548
|
const result = object(await request("/rest/posts", context, {
|
|
293
549
|
author: target.account.accountId,
|
|
294
|
-
commentary: target.content.text ?? "",
|
|
550
|
+
commentary: escapeCommentary(target.content.text ?? ""),
|
|
295
551
|
visibility: "PUBLIC",
|
|
296
552
|
distribution: {
|
|
297
553
|
feedDistribution: "MAIN_FEED",
|
|
@@ -300,6 +556,7 @@ export function linkedin(options) {
|
|
|
300
556
|
},
|
|
301
557
|
lifecycleState: "PUBLISHED",
|
|
302
558
|
isReshareDisabledByAuthor: false,
|
|
559
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
303
560
|
...(content ? { content } : {}),
|
|
304
561
|
}, ["x-restli-id"]));
|
|
305
562
|
const id = optionalString(object(result["headers"])["x-restli-id"]);
|
|
@@ -376,11 +633,16 @@ export function linkedin(options) {
|
|
|
376
633
|
const hasNext = array(paging["links"] ?? []).some((link) => object(link)["rel"] === "next");
|
|
377
634
|
return {
|
|
378
635
|
items,
|
|
636
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
379
637
|
...(items.length > 0 && (hasNext || (total !== undefined && start + items.length < total))
|
|
380
638
|
? { nextCursor: String(start + items.length) }
|
|
381
639
|
: {}),
|
|
382
640
|
};
|
|
383
641
|
},
|
|
642
|
+
async removeFromPlatform(ref, context) {
|
|
643
|
+
authorize(ref, context);
|
|
644
|
+
await request(`/rest/posts/${encodeURIComponent(ref.postId)}`, context, undefined, undefined, "DELETE");
|
|
645
|
+
},
|
|
384
646
|
},
|
|
385
647
|
comments: {
|
|
386
648
|
async list(ref, input, context) {
|
|
@@ -410,6 +672,7 @@ export function linkedin(options) {
|
|
|
410
672
|
const next = total !== undefined && start + items.length < total
|
|
411
673
|
? String(start + items.length)
|
|
412
674
|
: undefined;
|
|
675
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
413
676
|
return { items, ...(next === undefined ? {} : { nextCursor: next }) };
|
|
414
677
|
},
|
|
415
678
|
async reply(ref, content, context) {
|
|
@@ -420,21 +683,24 @@ export function linkedin(options) {
|
|
|
420
683
|
message: "Provide a comment of 1 to 1,250 characters.",
|
|
421
684
|
});
|
|
422
685
|
await readCommentablePost({ ...ref, kind: "platform-post" }, context);
|
|
423
|
-
const match = /^urn:li:comment:\(urn:li:activity
|
|
686
|
+
const match = /^urn:li:comment:\(urn:li:activity:(\d+),(\d+)\)$/.exec(ref.commentId);
|
|
424
687
|
if (!match)
|
|
425
688
|
throw new SocialError({
|
|
426
689
|
code: "invalid_input",
|
|
427
690
|
operation: "comments.write",
|
|
428
691
|
message: "Use the complete commentUrn returned by LinkedIn comment reads.",
|
|
429
692
|
});
|
|
430
|
-
const parent = object(await request(`/rest/socialActions/${encodeURIComponent(ref.postId)}/comments/${match[
|
|
693
|
+
const parent = object(await request(`/rest/socialActions/${encodeURIComponent(ref.postId)}/comments/${match[2]}`, context));
|
|
431
694
|
if (parent["commentUrn"] !== ref.commentId)
|
|
432
695
|
throw new SocialError({
|
|
433
696
|
code: "unauthorized",
|
|
434
697
|
operation: "comments.write",
|
|
435
698
|
message: "Comment does not belong to the supplied post.",
|
|
436
699
|
});
|
|
437
|
-
|
|
700
|
+
const parentObject = optionalString(parent["object"]);
|
|
701
|
+
if (parentObject !== undefined &&
|
|
702
|
+
parentObject !== ref.postId &&
|
|
703
|
+
parentObject !== `urn:li:activity:${match[1]}`)
|
|
438
704
|
throw new SocialError({
|
|
439
705
|
code: "unauthorized",
|
|
440
706
|
operation: "comments.write",
|
|
@@ -525,46 +791,73 @@ export function linkedin(options) {
|
|
|
525
791
|
});
|
|
526
792
|
return publicFields(image, ["id", "owner", "status"]);
|
|
527
793
|
},
|
|
528
|
-
async registerVideo({ account,
|
|
794
|
+
async registerVideo({ account, context }) {
|
|
529
795
|
authorize(account, context);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
796
|
+
throw new SocialError({
|
|
797
|
+
code: "unsupported_capability",
|
|
798
|
+
operation: "posts.video",
|
|
799
|
+
message: "LinkedIn video publishing is not implemented by this adapter.",
|
|
800
|
+
});
|
|
533
801
|
},
|
|
534
|
-
async createPoll({ account, text, options: pollOptions, context }) {
|
|
802
|
+
async createPoll({ account, text, options: pollOptions, duration = "THREE_DAYS", context }) {
|
|
535
803
|
authorize(account, context);
|
|
536
|
-
|
|
804
|
+
const result = object(await request("/rest/posts", context, {
|
|
537
805
|
author: account.accountId,
|
|
538
|
-
commentary: text,
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
806
|
+
commentary: escapeCommentary(text),
|
|
807
|
+
visibility: "PUBLIC",
|
|
808
|
+
distribution: {
|
|
809
|
+
feedDistribution: "MAIN_FEED",
|
|
810
|
+
targetEntities: [],
|
|
811
|
+
thirdPartyDistributionChannels: [],
|
|
812
|
+
},
|
|
813
|
+
lifecycleState: "PUBLISHED",
|
|
814
|
+
content: {
|
|
815
|
+
poll: {
|
|
816
|
+
question: text,
|
|
817
|
+
options: pollOptions.map((option) => ({ text: option })),
|
|
818
|
+
settings: { duration },
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
}, ["x-restli-id"]));
|
|
822
|
+
const id = optionalString(object(result["headers"])["x-restli-id"]);
|
|
823
|
+
// SAFETY: result is validated as a JSON object and id is a JSON string.
|
|
824
|
+
return (id === undefined ? result : { ...result, id });
|
|
542
825
|
},
|
|
543
826
|
async react({ account, postId, reaction, context }) {
|
|
544
827
|
authorize(account, context);
|
|
545
|
-
await request(`/rest/reactions`, context, {
|
|
828
|
+
await request(`/rest/reactions?actor=${encodeURIComponent(account.accountId)}`, context, {
|
|
546
829
|
root: postId,
|
|
547
830
|
reactionType: reaction,
|
|
548
|
-
actor: account.accountId,
|
|
549
831
|
});
|
|
550
832
|
},
|
|
551
833
|
async reshare({ account, postId, context }) {
|
|
552
834
|
authorize(account, context);
|
|
553
|
-
|
|
835
|
+
const result = object(await request("/rest/posts", context, {
|
|
554
836
|
author: account.accountId,
|
|
555
|
-
resharedPost: postId,
|
|
556
837
|
commentary: "",
|
|
557
|
-
|
|
838
|
+
visibility: "PUBLIC",
|
|
839
|
+
distribution: {
|
|
840
|
+
feedDistribution: "MAIN_FEED",
|
|
841
|
+
targetEntities: [],
|
|
842
|
+
thirdPartyDistributionChannels: [],
|
|
843
|
+
},
|
|
844
|
+
lifecycleState: "PUBLISHED",
|
|
845
|
+
reshareContext: { parent: postId },
|
|
846
|
+
}, ["x-restli-id"]));
|
|
847
|
+
const id = optionalString(object(result["headers"])["x-restli-id"]);
|
|
848
|
+
// SAFETY: result is validated as a JSON object and id is a JSON string.
|
|
849
|
+
return (id === undefined ? result : { ...result, id });
|
|
558
850
|
},
|
|
559
851
|
async updatePost({ account, postId, body, context }) {
|
|
560
852
|
authorize(account, context);
|
|
561
|
-
|
|
853
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
854
|
+
return (await request(`/rest/posts/${encodeURIComponent(postId)}`, context, { patch: { $set: body } }, ["x-restli-id"], "POST", { "X-RestLi-Method": "PARTIAL_UPDATE" }));
|
|
562
855
|
},
|
|
563
856
|
async deletePost({ account, postId, context }) {
|
|
564
857
|
authorize(account, context);
|
|
565
858
|
await request(`/rest/posts/${encodeURIComponent(postId)}`, context, undefined, undefined, "DELETE");
|
|
566
859
|
},
|
|
567
|
-
async organizationAnalytics({ account,
|
|
860
|
+
async organizationAnalytics({ account, context }) {
|
|
568
861
|
authorize(account, context);
|
|
569
862
|
if (!account.accountId.startsWith("urn:li:organization:"))
|
|
570
863
|
throw new SocialError({
|
|
@@ -572,7 +865,25 @@ export function linkedin(options) {
|
|
|
572
865
|
operation: "analytics.organization.read",
|
|
573
866
|
message: "Organization analytics requires an organization author.",
|
|
574
867
|
});
|
|
575
|
-
|
|
868
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
869
|
+
return (await request(`/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(account.accountId)}`, context));
|
|
870
|
+
},
|
|
871
|
+
async getOrganizationFollowerStatistics({ account, interval, context }) {
|
|
872
|
+
organizationOnly(account, context, "analytics.followers.read");
|
|
873
|
+
return parseFollowerStatistics(object(await request(statisticsPath("/rest/organizationalEntityFollowerStatistics", "organizationalEntity", account, interval), context)), interval?.granularity);
|
|
874
|
+
},
|
|
875
|
+
async getOrganizationPageStatistics({ account, interval, context }) {
|
|
876
|
+
organizationOnly(account, context, "analytics.page.read");
|
|
877
|
+
return parsePageStatistics(object(await request(statisticsPath("/rest/organizationPageStatistics", "organization", account, interval), context)), interval?.granularity);
|
|
878
|
+
},
|
|
879
|
+
async getOrganizationShareStatistics({ account, interval, context }) {
|
|
880
|
+
organizationOnly(account, context, "analytics.shares.read");
|
|
881
|
+
return parseShareStatistics(object(await request(statisticsPath("/rest/organizationalEntityShareStatistics", "organizationalEntity", account, interval), context)), interval?.granularity);
|
|
882
|
+
},
|
|
883
|
+
async getOrganizationFollowerCount({ account, context }) {
|
|
884
|
+
organizationOnly(account, context, "analytics.account.read");
|
|
885
|
+
const response = object(await request(`/rest/networkSizes/${encodeURIComponent(account.accountId)}?edgeType=COMPANY_FOLLOWED_BY_MEMBER`, context));
|
|
886
|
+
return optionalNumber(response["firstDegreeSize"]);
|
|
576
887
|
},
|
|
577
888
|
},
|
|
578
889
|
});
|