@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
|
@@ -1,18 +1,26 @@
|
|
|
1
|
+
/* oxlint-disable anti-slop/no-conditional-empty-object-spread, anti-slop/no-runtime-typeof, anti-slop/require-safety-comment-for-type-assertion, anti-slop/require-readable-spacing, anti-slop/no-chained-type-assertions -- validated external boundary or fixture contract. */
|
|
1
2
|
import { remainingBudget } from "../transport/budget.js";
|
|
2
3
|
import { defineAdapter } from "../core/adapter.js";
|
|
3
4
|
import { SocialError } from "../core/errors.js";
|
|
4
5
|
import { managedHttp, publicFields } from "../cloud/common.js";
|
|
6
|
+
import { createHttp, HttpError } from "../transport/http.js";
|
|
5
7
|
import { array, object, optionalString, string } from "../transport/validation.js";
|
|
6
8
|
import { beginYouTubeUpload, queryYouTubeUpload, sendYouTubeUpload, } from "./youtube-upload.js";
|
|
7
9
|
export function youtube(options) {
|
|
8
10
|
const request = managedHttp("https://www.googleapis.com", {
|
|
9
11
|
apiKey: options.auth.accessToken,
|
|
12
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
10
13
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
11
14
|
});
|
|
12
15
|
const analyticsRequest = managedHttp("https://youtubeanalytics.googleapis.com", {
|
|
13
16
|
apiKey: options.auth.accessToken,
|
|
17
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
14
18
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
15
19
|
});
|
|
20
|
+
const binaryRequest = createHttp({
|
|
21
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
22
|
+
timeoutMs: 30_000,
|
|
23
|
+
});
|
|
16
24
|
const now = () => (options.clock?.() ?? new Date()).toISOString();
|
|
17
25
|
const authorize = (ref, context) => {
|
|
18
26
|
if (ref.backend !== context.backendInstance ||
|
|
@@ -48,7 +56,9 @@ export function youtube(options) {
|
|
|
48
56
|
});
|
|
49
57
|
return video;
|
|
50
58
|
};
|
|
51
|
-
const outcome = (
|
|
59
|
+
const outcome = (
|
|
60
|
+
// oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- validated boundary or fixture contract.
|
|
61
|
+
video, target) => {
|
|
52
62
|
const id = string(video["id"]);
|
|
53
63
|
const status = object(video["status"]);
|
|
54
64
|
const uploaded = optionalString(status["uploadStatus"]) ?? "unknown";
|
|
@@ -123,6 +133,56 @@ export function youtube(options) {
|
|
|
123
133
|
status: "connected",
|
|
124
134
|
};
|
|
125
135
|
};
|
|
136
|
+
const nativeAuthorize = (context) => authorize({ backend: context.backendInstance, platform: "youtube", accountId: options.auth.channelId }, context);
|
|
137
|
+
const mediaBlob = (media) => {
|
|
138
|
+
if (!media || media.source.kind !== "blob")
|
|
139
|
+
throw new SocialError({
|
|
140
|
+
code: "invalid_input",
|
|
141
|
+
operation: "native",
|
|
142
|
+
message: "This YouTube operation requires a Blob media source.",
|
|
143
|
+
});
|
|
144
|
+
return media.source.blob;
|
|
145
|
+
};
|
|
146
|
+
const binaryJson = async (path, context, body, query, method = "POST", contentType) => {
|
|
147
|
+
const url = new URL(`https://www.googleapis.com${path}`);
|
|
148
|
+
for (const [key, value] of Object.entries(query))
|
|
149
|
+
url.searchParams.set(key, value);
|
|
150
|
+
try {
|
|
151
|
+
const result = await binaryRequest({
|
|
152
|
+
url,
|
|
153
|
+
method,
|
|
154
|
+
body,
|
|
155
|
+
headers: {
|
|
156
|
+
Authorization: `Bearer ${options.auth.accessToken}`,
|
|
157
|
+
...(contentType ? { "Content-Type": contentType } : {}),
|
|
158
|
+
},
|
|
159
|
+
timeoutMs: remainingBudget(context),
|
|
160
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
161
|
+
});
|
|
162
|
+
return object(result);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (!(error instanceof HttpError))
|
|
166
|
+
throw error;
|
|
167
|
+
const ambiguous = error.dispatched && (error.kind !== "http" || (error.status ?? 0) >= 500);
|
|
168
|
+
throw new SocialError({
|
|
169
|
+
code: ambiguous
|
|
170
|
+
? "ambiguous_outcome"
|
|
171
|
+
: error.status === 401
|
|
172
|
+
? "reconnect_required"
|
|
173
|
+
: error.status === 403
|
|
174
|
+
? "missing_permission"
|
|
175
|
+
: error.status === 429
|
|
176
|
+
? "rate_limited"
|
|
177
|
+
: "upstream_failure",
|
|
178
|
+
operation: path,
|
|
179
|
+
message: error.message,
|
|
180
|
+
upstreamStatus: error.status,
|
|
181
|
+
retryDisposition: ambiguous ? { kind: "reconcile-first" } : { kind: "never" },
|
|
182
|
+
cause: error,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
};
|
|
126
186
|
return defineAdapter({
|
|
127
187
|
id: "youtube",
|
|
128
188
|
capabilities: {
|
|
@@ -158,6 +218,25 @@ export function youtube(options) {
|
|
|
158
218
|
platform: "youtube",
|
|
159
219
|
availability: "available",
|
|
160
220
|
requiredScopes: ["https://www.googleapis.com/auth/youtube.upload"],
|
|
221
|
+
notes: "Scheduled videos are uploaded private with a future ISO publishAt timestamp.",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
operation: "posts.removeFromPlatform",
|
|
225
|
+
platform: "youtube",
|
|
226
|
+
availability: "available",
|
|
227
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube"],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
operation: "playlists.read",
|
|
231
|
+
platform: "youtube",
|
|
232
|
+
availability: "available",
|
|
233
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube.readonly"],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
operation: "playlists.write",
|
|
237
|
+
platform: "youtube",
|
|
238
|
+
availability: "available",
|
|
239
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube"],
|
|
161
240
|
},
|
|
162
241
|
{
|
|
163
242
|
operation: "thumbnails.write",
|
|
@@ -165,6 +244,36 @@ export function youtube(options) {
|
|
|
165
244
|
availability: "available",
|
|
166
245
|
requiredScopes: ["https://www.googleapis.com/auth/youtube"],
|
|
167
246
|
},
|
|
247
|
+
{
|
|
248
|
+
operation: "videos.update",
|
|
249
|
+
platform: "youtube",
|
|
250
|
+
availability: "available",
|
|
251
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube"],
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
operation: "posts.update",
|
|
255
|
+
platform: "youtube",
|
|
256
|
+
availability: "available",
|
|
257
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube"],
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
operation: "videos.delete",
|
|
261
|
+
platform: "youtube",
|
|
262
|
+
availability: "available",
|
|
263
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube"],
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
operation: "posts.delete",
|
|
267
|
+
platform: "youtube",
|
|
268
|
+
availability: "available",
|
|
269
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube"],
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
operation: "videos.rate",
|
|
273
|
+
platform: "youtube",
|
|
274
|
+
availability: "available",
|
|
275
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube.force-ssl"],
|
|
276
|
+
},
|
|
168
277
|
{
|
|
169
278
|
operation: "captions.read",
|
|
170
279
|
platform: "youtube",
|
|
@@ -177,17 +286,52 @@ export function youtube(options) {
|
|
|
177
286
|
availability: "available",
|
|
178
287
|
requiredScopes: ["https://www.googleapis.com/auth/youtube.force-ssl"],
|
|
179
288
|
},
|
|
180
|
-
{
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
289
|
+
{
|
|
290
|
+
operation: "subscriptions.read",
|
|
291
|
+
platform: "youtube",
|
|
292
|
+
availability: "available",
|
|
293
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube.readonly"],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
operation: "subscriptions.write",
|
|
297
|
+
platform: "youtube",
|
|
298
|
+
availability: "available",
|
|
299
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube"],
|
|
300
|
+
},
|
|
301
|
+
{ operation: "search.keyword", platform: "youtube", availability: "available" },
|
|
302
|
+
{
|
|
303
|
+
operation: "search.posts",
|
|
304
|
+
platform: "youtube",
|
|
305
|
+
availability: "available",
|
|
306
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube.readonly"],
|
|
307
|
+
notes: "Uses search.list with type=video. Each request costs 100 quota units.",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
operation: "comments.moderate",
|
|
311
|
+
platform: "youtube",
|
|
312
|
+
availability: "available",
|
|
313
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube.force-ssl"],
|
|
314
|
+
},
|
|
184
315
|
{
|
|
185
316
|
operation: "analytics.youtube.read",
|
|
186
317
|
platform: "youtube",
|
|
187
318
|
availability: "available",
|
|
188
319
|
requiredScopes: ["https://www.googleapis.com/auth/yt-analytics.readonly"],
|
|
189
320
|
},
|
|
190
|
-
{
|
|
321
|
+
{
|
|
322
|
+
operation: "analytics.report.read",
|
|
323
|
+
platform: "youtube",
|
|
324
|
+
availability: "available",
|
|
325
|
+
requiredScopes: ["https://www.googleapis.com/auth/yt-analytics.readonly"],
|
|
326
|
+
notes: "Reports require an explicit bounded date range and provider-supported metrics.",
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
operation: "live.broadcasts",
|
|
330
|
+
platform: "youtube",
|
|
331
|
+
availability: "available",
|
|
332
|
+
requiredScopes: ["https://www.googleapis.com/auth/youtube"],
|
|
333
|
+
notes: "The channel must have live streaming enabled in YouTube Studio. Use native access: liveBroadcasts.",
|
|
334
|
+
},
|
|
191
335
|
],
|
|
192
336
|
},
|
|
193
337
|
accounts: {
|
|
@@ -227,6 +371,7 @@ export function youtube(options) {
|
|
|
227
371
|
playlistId: uploads,
|
|
228
372
|
part: "snippet,contentDetails",
|
|
229
373
|
maxResults: String(limit),
|
|
374
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
230
375
|
...(input.cursor ? { pageToken: input.cursor } : {}),
|
|
231
376
|
}));
|
|
232
377
|
return {
|
|
@@ -252,6 +397,14 @@ export function youtube(options) {
|
|
|
252
397
|
contentDetails: publicFields(content, ["videoId", "videoPublishedAt"]),
|
|
253
398
|
};
|
|
254
399
|
}),
|
|
400
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
401
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
402
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
403
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
404
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
405
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
406
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
407
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
255
408
|
...(typeof page["nextPageToken"] === "string"
|
|
256
409
|
? { nextCursor: page["nextPageToken"] }
|
|
257
410
|
: {}),
|
|
@@ -276,16 +429,27 @@ export function youtube(options) {
|
|
|
276
429
|
fail("youtube.size", "Streaming upload requires its exact byte size.");
|
|
277
430
|
}
|
|
278
431
|
const config = target.options === undefined ? {} : object(target.options);
|
|
279
|
-
if (
|
|
432
|
+
if (
|
|
433
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
434
|
+
typeof config["title"] !== "string" ||
|
|
280
435
|
!config["title"] ||
|
|
281
|
-
[...config["title"]].length > 100
|
|
436
|
+
[...config["title"]].length > 100 ||
|
|
437
|
+
/[<>]/u.test(config["title"]))
|
|
282
438
|
fail("youtube.title", "Select a title of 1 to 100 characters.");
|
|
283
439
|
if (!["public", "unlisted", "private"].includes(String(config["visibility"])))
|
|
284
440
|
fail("youtube.visibility", "Explicit visibility is required.");
|
|
441
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
285
442
|
if (typeof config["madeForKids"] !== "boolean")
|
|
286
443
|
fail("youtube.audience", "Explicit made-for-kids declaration is required.");
|
|
287
|
-
if (target.
|
|
288
|
-
|
|
444
|
+
if (target.content.text !== undefined &&
|
|
445
|
+
new TextEncoder().encode(target.content.text).byteLength > 5000)
|
|
446
|
+
fail("youtube.description", "Description must be at most 5000 UTF-8 bytes.");
|
|
447
|
+
if (target.schedule &&
|
|
448
|
+
(!Number.isFinite(Date.parse(target.schedule.at)) ||
|
|
449
|
+
Date.parse(target.schedule.at) <= (options.clock?.() ?? new Date()).getTime()))
|
|
450
|
+
fail("youtube.schedule", "Schedule time must be a valid timestamp in the future.");
|
|
451
|
+
if (target.replyTo)
|
|
452
|
+
fail("youtube.operation", "Use comments.reply for replies.");
|
|
289
453
|
return issues;
|
|
290
454
|
},
|
|
291
455
|
async publishTarget(target, context) {
|
|
@@ -299,9 +463,11 @@ export function youtube(options) {
|
|
|
299
463
|
});
|
|
300
464
|
const config = object(target.options);
|
|
301
465
|
const uploadOptions = {
|
|
302
|
-
timeoutMs: remainingBudget(context),
|
|
466
|
+
timeoutMs: Math.max(remainingBudget(context), 15 * 60_000),
|
|
303
467
|
accessToken: options.auth.accessToken,
|
|
468
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
304
469
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
470
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
305
471
|
...(context.signal ? { signal: context.signal } : {}),
|
|
306
472
|
};
|
|
307
473
|
const size = media.byteSize ?? (media.source.kind === "blob" ? media.source.blob.size : 0);
|
|
@@ -314,6 +480,12 @@ export function youtube(options) {
|
|
|
314
480
|
status: {
|
|
315
481
|
privacyStatus: string(config["visibility"]),
|
|
316
482
|
selfDeclaredMadeForKids: config["madeForKids"] === true,
|
|
483
|
+
...(target.schedule
|
|
484
|
+
? {
|
|
485
|
+
publishAt: new Date(target.schedule.at).toISOString(),
|
|
486
|
+
privacyStatus: "private",
|
|
487
|
+
}
|
|
488
|
+
: {}),
|
|
317
489
|
},
|
|
318
490
|
},
|
|
319
491
|
}, uploadOptions);
|
|
@@ -359,6 +531,48 @@ export function youtube(options) {
|
|
|
359
531
|
const video = await get({ ...account, kind: "platform-post", postId: ref.deliveryId }, context);
|
|
360
532
|
return outcome(video, { account, targetIndex: 0 });
|
|
361
533
|
},
|
|
534
|
+
async removeFromPlatform(ref, context) {
|
|
535
|
+
authorize(ref, context);
|
|
536
|
+
if (!ref.postId)
|
|
537
|
+
throw new SocialError({
|
|
538
|
+
code: "invalid_input",
|
|
539
|
+
operation: "posts.removeFromPlatform",
|
|
540
|
+
message: "postId is required.",
|
|
541
|
+
});
|
|
542
|
+
await request("/youtube/v3/videos", context, undefined, { id: ref.postId }, "DELETE");
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
search: {
|
|
546
|
+
async posts(account, input, context) {
|
|
547
|
+
authorize(account, context);
|
|
548
|
+
const limit = input.limit ?? 25;
|
|
549
|
+
if (input.scope !== undefined && input.scope !== "recent")
|
|
550
|
+
throw new SocialError({
|
|
551
|
+
code: "invalid_input",
|
|
552
|
+
operation: "search.posts",
|
|
553
|
+
message: "YouTube search supports only the recent scope.",
|
|
554
|
+
});
|
|
555
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50)
|
|
556
|
+
throw new SocialError({
|
|
557
|
+
code: "invalid_input",
|
|
558
|
+
operation: "search.posts",
|
|
559
|
+
message: "YouTube search limit must be between 1 and 50.",
|
|
560
|
+
});
|
|
561
|
+
const page = object(await request("/youtube/v3/search", context, undefined, {
|
|
562
|
+
part: "snippet",
|
|
563
|
+
q: input.query,
|
|
564
|
+
type: "video",
|
|
565
|
+
maxResults: String(limit),
|
|
566
|
+
...(input.cursor ? { pageToken: input.cursor } : {}),
|
|
567
|
+
...(input.startTime ? { publishedAfter: input.startTime } : {}),
|
|
568
|
+
...(input.endTime ? { publishedBefore: input.endTime } : {}),
|
|
569
|
+
}));
|
|
570
|
+
const nextCursor = optionalString(page["nextPageToken"]);
|
|
571
|
+
return {
|
|
572
|
+
items: array(page["items"]).map((value) => object(value)),
|
|
573
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
574
|
+
};
|
|
575
|
+
},
|
|
362
576
|
},
|
|
363
577
|
analytics: {
|
|
364
578
|
async getPostMetrics(ref, context) {
|
|
@@ -369,6 +583,7 @@ export function youtube(options) {
|
|
|
369
583
|
const metrics = [];
|
|
370
584
|
for (const name of ["viewCount", "likeCount", "commentCount"]) {
|
|
371
585
|
const raw = values[name];
|
|
586
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
372
587
|
if (typeof raw !== "string" || !/^\d+$/.test(raw))
|
|
373
588
|
continue;
|
|
374
589
|
const value = Number(raw);
|
|
@@ -408,6 +623,7 @@ export function youtube(options) {
|
|
|
408
623
|
if (name === "subscriberCount" && stats["hiddenSubscriberCount"] === true)
|
|
409
624
|
return [];
|
|
410
625
|
const raw = stats[name];
|
|
626
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
411
627
|
return typeof raw === "string" && /^\d+$/.test(raw) && Number.isSafeInteger(Number(raw))
|
|
412
628
|
? [
|
|
413
629
|
{
|
|
@@ -423,6 +639,68 @@ export function youtube(options) {
|
|
|
423
639
|
: [];
|
|
424
640
|
});
|
|
425
641
|
},
|
|
642
|
+
async getReport(accountRef, query, context) {
|
|
643
|
+
authorize(accountRef, context);
|
|
644
|
+
const date = /^\d{4}-\d{2}-\d{2}$/;
|
|
645
|
+
if (!date.test(query.from) ||
|
|
646
|
+
!date.test(query.to) ||
|
|
647
|
+
query.from > query.to ||
|
|
648
|
+
query.metrics.length === 0 ||
|
|
649
|
+
query.metrics.some((metric) => !/^[A-Za-z][A-Za-z0-9_.-]*$/.test(metric)) ||
|
|
650
|
+
query.dimensions?.some((dimension) => !/^[A-Za-z][A-Za-z0-9_.-]*$/.test(dimension)))
|
|
651
|
+
throw new SocialError({
|
|
652
|
+
code: "invalid_input",
|
|
653
|
+
operation: "analytics.report.read",
|
|
654
|
+
message: "YouTube reports require YYYY-MM-DD dates (from <= to) and provider-safe metric names.",
|
|
655
|
+
});
|
|
656
|
+
const result = object(await analyticsRequest("/v2/reports", context, undefined, {
|
|
657
|
+
ids: `channel==${options.auth.channelId}`,
|
|
658
|
+
startDate: query.from,
|
|
659
|
+
endDate: query.to,
|
|
660
|
+
metrics: query.metrics.join(","),
|
|
661
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
662
|
+
...(query.dimensions && query.dimensions.length > 0
|
|
663
|
+
? { dimensions: query.dimensions.join(",") }
|
|
664
|
+
: {}),
|
|
665
|
+
}));
|
|
666
|
+
const headers = array(result["columnHeaders"]).map((value) => {
|
|
667
|
+
const header = object(value);
|
|
668
|
+
return {
|
|
669
|
+
name: string(header["name"]),
|
|
670
|
+
type: optionalString(header["columnType"]) ?? "METRIC",
|
|
671
|
+
};
|
|
672
|
+
});
|
|
673
|
+
const rows = [];
|
|
674
|
+
// Analytics omits `rows` entirely when the requested period has no data.
|
|
675
|
+
for (const value of Array.isArray(result["rows"]) ? result["rows"] : []) {
|
|
676
|
+
const cells = array(value);
|
|
677
|
+
const dimensions = Object.fromEntries(headers.flatMap((header, index) => {
|
|
678
|
+
if (header.type !== "DIMENSION")
|
|
679
|
+
return [];
|
|
680
|
+
const raw = cells[index];
|
|
681
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
682
|
+
return typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean"
|
|
683
|
+
? [[header.name, raw]]
|
|
684
|
+
: [];
|
|
685
|
+
}));
|
|
686
|
+
const metrics = {};
|
|
687
|
+
headers.forEach((header, index) => {
|
|
688
|
+
if (header.type === "DIMENSION")
|
|
689
|
+
return;
|
|
690
|
+
const raw = cells[index];
|
|
691
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
692
|
+
const parsed = typeof raw === "number"
|
|
693
|
+
? raw
|
|
694
|
+
: typeof raw === "string" && raw.trim() !== ""
|
|
695
|
+
? Number(raw)
|
|
696
|
+
: undefined;
|
|
697
|
+
if (parsed !== undefined && Number.isFinite(parsed))
|
|
698
|
+
metrics[header.name] = parsed;
|
|
699
|
+
});
|
|
700
|
+
rows.push({ dimensions, metrics });
|
|
701
|
+
}
|
|
702
|
+
return { query, rows, fetchedAt: now(), source: "youtube-analytics-v2" };
|
|
703
|
+
},
|
|
426
704
|
},
|
|
427
705
|
comments: {
|
|
428
706
|
async list(ref, input, context) {
|
|
@@ -439,6 +717,7 @@ export function youtube(options) {
|
|
|
439
717
|
videoId: ref.postId,
|
|
440
718
|
textFormat: "plainText",
|
|
441
719
|
maxResults: String(limit),
|
|
720
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
442
721
|
...(input.cursor === undefined ? {} : { pageToken: input.cursor }),
|
|
443
722
|
}));
|
|
444
723
|
const cursor = optionalString(result["nextPageToken"]);
|
|
@@ -456,6 +735,7 @@ export function youtube(options) {
|
|
|
456
735
|
]),
|
|
457
736
|
};
|
|
458
737
|
}),
|
|
738
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
459
739
|
...(cursor ? { nextCursor: cursor } : {}),
|
|
460
740
|
};
|
|
461
741
|
},
|
|
@@ -488,7 +768,9 @@ export function youtube(options) {
|
|
|
488
768
|
const uploadOptions = {
|
|
489
769
|
timeoutMs: remainingBudget(context),
|
|
490
770
|
accessToken: options.auth.accessToken,
|
|
771
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
491
772
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
773
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
492
774
|
...(context.signal ? { signal: context.signal } : {}),
|
|
493
775
|
};
|
|
494
776
|
const confirmed = await queryYouTubeUpload(session, uploadOptions);
|
|
@@ -505,28 +787,131 @@ export function youtube(options) {
|
|
|
505
787
|
});
|
|
506
788
|
return queryYouTubeUpload(session, {
|
|
507
789
|
accessToken: options.auth.accessToken,
|
|
790
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
508
791
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
792
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
509
793
|
...(context.signal ? { signal: context.signal } : {}),
|
|
510
794
|
});
|
|
511
795
|
},
|
|
512
796
|
async setThumbnail({ videoId, thumbnail, context }) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
backend: context.backendInstance,
|
|
516
|
-
platform: "youtube",
|
|
517
|
-
accountId: options.auth.channelId,
|
|
518
|
-
}, context);
|
|
519
|
-
return object(await request("/youtube/v3/thumbnails/set", context, undefined, { videoId, uploadType: "media" }, "POST"));
|
|
797
|
+
nativeAuthorize(context);
|
|
798
|
+
return binaryJson("/upload/youtube/v3/thumbnails/set", context, mediaBlob(thumbnail), { videoId, uploadType: "media" }, "POST", thumbnail.mimeType ?? "application/octet-stream");
|
|
520
799
|
},
|
|
521
|
-
async captions({ action, videoId, captionId, context }) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
800
|
+
async captions({ action, videoId, captionId, caption, body, context }) {
|
|
801
|
+
nativeAuthorize(context);
|
|
802
|
+
if (action === "download") {
|
|
803
|
+
if (!captionId)
|
|
804
|
+
throw new SocialError({
|
|
805
|
+
code: "invalid_input",
|
|
806
|
+
operation: "captions.download",
|
|
807
|
+
message: "captionId is required.",
|
|
808
|
+
});
|
|
809
|
+
const url = new URL(`https://www.googleapis.com/youtube/v3/captions/${encodeURIComponent(captionId)}`);
|
|
810
|
+
url.searchParams.set("tfmt", "vtt");
|
|
811
|
+
const response = await (options.fetch ?? globalThis.fetch)(url, {
|
|
812
|
+
headers: { Authorization: `Bearer ${options.auth.accessToken}` },
|
|
813
|
+
redirect: "error",
|
|
814
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
815
|
+
});
|
|
816
|
+
if (!response.ok)
|
|
817
|
+
throw new SocialError({
|
|
818
|
+
code: "upstream_failure",
|
|
819
|
+
operation: "captions.download",
|
|
820
|
+
message: `YouTube returned HTTP ${response.status}.`,
|
|
821
|
+
upstreamStatus: response.status,
|
|
822
|
+
retryDisposition: { kind: "never" },
|
|
823
|
+
});
|
|
824
|
+
return response.blob();
|
|
825
|
+
}
|
|
826
|
+
if (action === "insert" || action === "update") {
|
|
827
|
+
if (!body || (action === "insert" && !caption))
|
|
828
|
+
throw new SocialError({
|
|
829
|
+
code: "invalid_input",
|
|
830
|
+
operation: `captions.${action}`,
|
|
831
|
+
message: "Caption metadata and media are required for insert.",
|
|
832
|
+
});
|
|
833
|
+
const snippet = object(body["snippet"] ?? {});
|
|
834
|
+
if (action === "insert" && (!videoId || (snippet["videoId"] ?? videoId) !== videoId))
|
|
835
|
+
throw new SocialError({
|
|
836
|
+
code: "invalid_input",
|
|
837
|
+
operation: "captions.insert",
|
|
838
|
+
message: "videoId is required and must match body.snippet.videoId.",
|
|
839
|
+
});
|
|
840
|
+
const metadata = action === "update"
|
|
841
|
+
? { ...body, id: captionId ?? body["id"] ?? null }
|
|
842
|
+
: { ...body, snippet: { ...snippet, videoId: videoId ?? null } };
|
|
843
|
+
if (action === "update" && typeof metadata["id"] !== "string")
|
|
844
|
+
throw new SocialError({
|
|
845
|
+
code: "invalid_input",
|
|
846
|
+
operation: "captions.update",
|
|
847
|
+
message: "captionId or body.id is required.",
|
|
848
|
+
});
|
|
849
|
+
if (action === "update" && !caption)
|
|
850
|
+
return binaryJson("/youtube/v3/captions", context, JSON.stringify(metadata), { part: "snippet" }, "PUT", "application/json");
|
|
851
|
+
if (!caption)
|
|
852
|
+
throw new SocialError({
|
|
853
|
+
code: "invalid_input",
|
|
854
|
+
operation: "captions.insert",
|
|
855
|
+
message: "Caption media is required.",
|
|
856
|
+
});
|
|
857
|
+
const boundary = `youtube-caption-${crypto.randomUUID()}`;
|
|
858
|
+
const media = mediaBlob(caption);
|
|
859
|
+
const filename = (caption.filename ?? "captions.vtt").replace(/[\r\n]/gu, "");
|
|
860
|
+
const encoded = new Blob([
|
|
861
|
+
`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${JSON.stringify(metadata)}\r\n`,
|
|
862
|
+
`--${boundary}\r\nContent-Type: ${caption.mimeType ?? "text/vtt"}\r\nContent-Disposition: attachment; filename="${filename}"\r\n\r\n`,
|
|
863
|
+
media,
|
|
864
|
+
`\r\n--${boundary}--\r\n`,
|
|
865
|
+
]);
|
|
866
|
+
return binaryJson("/upload/youtube/v3/captions", context, encoded, { uploadType: "multipart", part: "snippet" }, action === "update" ? "PUT" : "POST", `multipart/related; boundary=${boundary}`);
|
|
867
|
+
}
|
|
868
|
+
if (action === "delete") {
|
|
869
|
+
if (!captionId)
|
|
870
|
+
throw new SocialError({
|
|
871
|
+
code: "invalid_input",
|
|
872
|
+
operation: "captions.delete",
|
|
873
|
+
message: "captionId is required.",
|
|
874
|
+
});
|
|
875
|
+
await request("/youtube/v3/captions", context, undefined, { id: captionId ?? "" }, "DELETE");
|
|
876
|
+
return {};
|
|
877
|
+
}
|
|
878
|
+
return object(await request("/youtube/v3/captions", context, undefined, { part: "snippet", videoId }, "GET"));
|
|
879
|
+
},
|
|
880
|
+
async playlists({ action, playlistId, channelId, mine, pageToken, maxResults, body, context, }) {
|
|
881
|
+
nativeAuthorize(context);
|
|
882
|
+
const method = action === "list"
|
|
883
|
+
? "GET"
|
|
884
|
+
: action === "delete"
|
|
885
|
+
? "DELETE"
|
|
886
|
+
: action === "update"
|
|
887
|
+
? "PUT"
|
|
888
|
+
: "POST";
|
|
889
|
+
if ((action === "delete" || action === "update") && !playlistId)
|
|
890
|
+
throw new SocialError({
|
|
891
|
+
code: "invalid_input",
|
|
892
|
+
operation: `playlists.${action}`,
|
|
893
|
+
message: "playlistId is required.",
|
|
894
|
+
});
|
|
895
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
896
|
+
if (action === "delete") {
|
|
897
|
+
await request("/youtube/v3/playlists", context, undefined, { id: playlistId ?? "" }, method);
|
|
898
|
+
return {};
|
|
899
|
+
}
|
|
900
|
+
return object(await request("/youtube/v3/playlists", context, body, {
|
|
901
|
+
part: "snippet,status,contentDetails",
|
|
902
|
+
...(playlistId ? { id: playlistId } : {}),
|
|
903
|
+
...(channelId ? { channelId } : {}),
|
|
904
|
+
...(action === "list" && !playlistId && !channelId
|
|
905
|
+
? { mine: "true" }
|
|
906
|
+
: mine
|
|
907
|
+
? { mine: "true" }
|
|
908
|
+
: {}),
|
|
909
|
+
...(pageToken ? { pageToken } : {}),
|
|
910
|
+
...(maxResults ? { maxResults: String(maxResults) } : {}),
|
|
527
911
|
}, method));
|
|
528
912
|
},
|
|
529
|
-
async
|
|
913
|
+
async playlistItems({ action, playlistId, playlistItemId, body, pageToken, context }) {
|
|
914
|
+
nativeAuthorize(context);
|
|
530
915
|
const method = action === "list"
|
|
531
916
|
? "GET"
|
|
532
917
|
: action === "delete"
|
|
@@ -534,25 +919,180 @@ export function youtube(options) {
|
|
|
534
919
|
: action === "update"
|
|
535
920
|
? "PUT"
|
|
536
921
|
: "POST";
|
|
537
|
-
|
|
922
|
+
if (action === "delete") {
|
|
923
|
+
if (!playlistItemId)
|
|
924
|
+
throw new SocialError({
|
|
925
|
+
code: "invalid_input",
|
|
926
|
+
operation: "playlistItems.delete",
|
|
927
|
+
message: "playlistItemId is required.",
|
|
928
|
+
});
|
|
929
|
+
await request("/youtube/v3/playlistItems", context, undefined, { id: playlistItemId }, method);
|
|
930
|
+
return {};
|
|
931
|
+
}
|
|
932
|
+
return object(await request("/youtube/v3/playlistItems", context, body, {
|
|
933
|
+
part: "snippet,contentDetails",
|
|
934
|
+
...(playlistId ? { playlistId } : {}),
|
|
935
|
+
...(playlistItemId ? { id: playlistItemId } : {}),
|
|
936
|
+
...(pageToken ? { pageToken } : {}),
|
|
937
|
+
}, method));
|
|
538
938
|
},
|
|
539
939
|
async updateVideo({ videoId, body, context }) {
|
|
540
|
-
|
|
940
|
+
nativeAuthorize(context);
|
|
941
|
+
if (!videoId)
|
|
942
|
+
throw new SocialError({
|
|
943
|
+
code: "invalid_input",
|
|
944
|
+
operation: "videos.update",
|
|
945
|
+
message: "videoId is required.",
|
|
946
|
+
});
|
|
947
|
+
const existing = await get({
|
|
948
|
+
kind: "platform-post",
|
|
949
|
+
version: 1,
|
|
950
|
+
backend: context.backendInstance,
|
|
951
|
+
platform: "youtube",
|
|
952
|
+
accountId: options.auth.channelId,
|
|
953
|
+
postId: videoId,
|
|
954
|
+
}, context);
|
|
955
|
+
const merged = {
|
|
956
|
+
...existing,
|
|
957
|
+
...body,
|
|
958
|
+
id: videoId,
|
|
959
|
+
snippet: { ...object(existing["snippet"]), ...object(body["snippet"] ?? {}) },
|
|
960
|
+
status: { ...object(existing["status"] ?? {}), ...object(body["status"] ?? {}) },
|
|
961
|
+
};
|
|
962
|
+
return object(await request("/youtube/v3/videos", context, merged, { part: "snippet,status" }, "PUT"));
|
|
541
963
|
},
|
|
542
964
|
async deleteVideo({ videoId, context }) {
|
|
965
|
+
nativeAuthorize(context);
|
|
966
|
+
if (!videoId)
|
|
967
|
+
throw new SocialError({
|
|
968
|
+
code: "invalid_input",
|
|
969
|
+
operation: "videos.delete",
|
|
970
|
+
message: "videoId is required.",
|
|
971
|
+
});
|
|
543
972
|
await request("/youtube/v3/videos", context, undefined, { id: videoId }, "DELETE");
|
|
544
973
|
},
|
|
974
|
+
async rateVideo({ videoId, rating, context }) {
|
|
975
|
+
nativeAuthorize(context);
|
|
976
|
+
await request("/youtube/v3/videos/rate", context, undefined, { id: videoId, rating }, "POST");
|
|
977
|
+
},
|
|
978
|
+
async getRating({ videoIds, context }) {
|
|
979
|
+
nativeAuthorize(context);
|
|
980
|
+
return object(await request("/youtube/v3/videos/getRating", context, undefined, {
|
|
981
|
+
id: videoIds.join(","),
|
|
982
|
+
}));
|
|
983
|
+
},
|
|
984
|
+
async subscriptions({ action, subscriptionId, channelId, pageToken, context }) {
|
|
985
|
+
nativeAuthorize(context);
|
|
986
|
+
const method = action === "list" ? "GET" : action === "delete" ? "DELETE" : "POST";
|
|
987
|
+
if (action === "delete" && !subscriptionId)
|
|
988
|
+
throw new SocialError({
|
|
989
|
+
code: "invalid_input",
|
|
990
|
+
operation: "subscriptions.delete",
|
|
991
|
+
message: "subscriptionId is required.",
|
|
992
|
+
});
|
|
993
|
+
const insertBody = channelId
|
|
994
|
+
? {
|
|
995
|
+
snippet: { resourceId: { kind: "youtube#channel", channelId } },
|
|
996
|
+
}
|
|
997
|
+
: undefined;
|
|
998
|
+
if (action === "delete") {
|
|
999
|
+
await request("/youtube/v3/subscriptions", context, undefined, { id: subscriptionId ?? "" }, method);
|
|
1000
|
+
return {};
|
|
1001
|
+
}
|
|
1002
|
+
return object(await request("/youtube/v3/subscriptions", context, insertBody, {
|
|
1003
|
+
part: "snippet,contentDetails",
|
|
1004
|
+
...(subscriptionId
|
|
1005
|
+
? { id: subscriptionId }
|
|
1006
|
+
: channelId
|
|
1007
|
+
? { channelId }
|
|
1008
|
+
: action === "list"
|
|
1009
|
+
? { mine: "true" }
|
|
1010
|
+
: {}),
|
|
1011
|
+
...(pageToken ? { pageToken } : {}),
|
|
1012
|
+
}, method));
|
|
1013
|
+
},
|
|
1014
|
+
async search({ q, type, channelId, order, publishedAfter, publishedBefore, pageToken, maxResults, context, }) {
|
|
1015
|
+
nativeAuthorize(context);
|
|
1016
|
+
return object(await request("/youtube/v3/search", context, undefined, {
|
|
1017
|
+
part: "snippet",
|
|
1018
|
+
q,
|
|
1019
|
+
...(type ? { type } : {}),
|
|
1020
|
+
...(channelId ? { channelId } : {}),
|
|
1021
|
+
...(order ? { order } : {}),
|
|
1022
|
+
...(publishedAfter ? { publishedAfter } : {}),
|
|
1023
|
+
...(publishedBefore ? { publishedBefore } : {}),
|
|
1024
|
+
...(pageToken ? { pageToken } : {}),
|
|
1025
|
+
...(maxResults ? { maxResults: String(maxResults) } : {}),
|
|
1026
|
+
}));
|
|
1027
|
+
},
|
|
1028
|
+
async commentsModeration({ action, commentId, moderationStatus, banAuthor, body, context }) {
|
|
1029
|
+
nativeAuthorize(context);
|
|
1030
|
+
if (!commentId)
|
|
1031
|
+
throw new SocialError({
|
|
1032
|
+
code: "invalid_input",
|
|
1033
|
+
operation: "comments.moderate",
|
|
1034
|
+
message: "commentId is required.",
|
|
1035
|
+
});
|
|
1036
|
+
if (action === "setModerationStatus") {
|
|
1037
|
+
await request("/youtube/v3/comments/setModerationStatus", context, undefined, {
|
|
1038
|
+
id: commentId,
|
|
1039
|
+
moderationStatus: moderationStatus ??
|
|
1040
|
+
(() => {
|
|
1041
|
+
throw new SocialError({
|
|
1042
|
+
code: "invalid_input",
|
|
1043
|
+
operation: "comments.moderate",
|
|
1044
|
+
message: "moderationStatus is required.",
|
|
1045
|
+
});
|
|
1046
|
+
})(),
|
|
1047
|
+
...(banAuthor ? { banAuthor: "true" } : {}),
|
|
1048
|
+
}, "POST");
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (action === "delete") {
|
|
1052
|
+
await request("/youtube/v3/comments", context, undefined, { id: commentId }, "DELETE");
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (!body || (body["id"] ?? commentId) !== commentId)
|
|
1056
|
+
throw new SocialError({
|
|
1057
|
+
code: "invalid_input",
|
|
1058
|
+
operation: "comments.update",
|
|
1059
|
+
message: "Provide a comment body whose id, if present, matches commentId.",
|
|
1060
|
+
});
|
|
1061
|
+
return object(await request("/youtube/v3/comments", context, { ...body, id: commentId }, { part: "snippet" }, "PUT"));
|
|
1062
|
+
},
|
|
1063
|
+
async heldComments({ pageToken, maxResults, context }) {
|
|
1064
|
+
nativeAuthorize(context);
|
|
1065
|
+
return object(await request("/youtube/v3/commentThreads", context, undefined, {
|
|
1066
|
+
part: "snippet",
|
|
1067
|
+
moderationStatus: "heldForReview",
|
|
1068
|
+
allThreadsRelatedToChannelId: options.auth.channelId,
|
|
1069
|
+
...(pageToken ? { pageToken } : {}),
|
|
1070
|
+
...(maxResults ? { maxResults: String(maxResults) } : {}),
|
|
1071
|
+
}));
|
|
1072
|
+
},
|
|
545
1073
|
async analytics({ query, context }) {
|
|
1074
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
546
1075
|
return object(await analyticsRequest("/v2/reports", context, undefined, query));
|
|
547
1076
|
},
|
|
548
|
-
async liveBroadcasts({ action, body, id, context }) {
|
|
1077
|
+
async liveBroadcasts({ action, body, id, broadcastStatus, context }) {
|
|
549
1078
|
if (action === "list")
|
|
1079
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
550
1080
|
return object(await request("/youtube/v3/liveBroadcasts", context, undefined, {
|
|
551
1081
|
part: "snippet,status",
|
|
552
|
-
|
|
1082
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
1083
|
+
...(id ? { id } : { mine: "true" }),
|
|
553
1084
|
}));
|
|
554
|
-
if (action === "transition")
|
|
555
|
-
|
|
1085
|
+
if (action === "transition") {
|
|
1086
|
+
if (!id || !broadcastStatus)
|
|
1087
|
+
throw new SocialError({
|
|
1088
|
+
code: "invalid_input",
|
|
1089
|
+
operation: "live.broadcasts",
|
|
1090
|
+
message: "id and broadcastStatus are required.",
|
|
1091
|
+
});
|
|
1092
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
1093
|
+
return object(await request("/youtube/v3/liveBroadcasts/transition", context, undefined, { part: "snippet,status", id, broadcastStatus }, "POST"));
|
|
1094
|
+
}
|
|
1095
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
556
1096
|
return object(await request("/youtube/v3/liveBroadcasts", context, body, { part: "snippet,status" }, "POST"));
|
|
557
1097
|
},
|
|
558
1098
|
},
|