@opencoredev/social-sdk 0.2.1 → 0.3.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/dist/platforms/x.d.ts +15 -0
- package/dist/platforms/x.js +336 -17
- package/package.json +1 -1
package/dist/platforms/x.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface XSearchInput extends SearchPostsInput {
|
|
|
23
23
|
readonly userFields?: readonly XUserField[];
|
|
24
24
|
readonly mediaFields?: readonly XMediaField[];
|
|
25
25
|
}
|
|
26
|
+
export interface XUploadedMedia {
|
|
27
|
+
readonly mediaId: string;
|
|
28
|
+
}
|
|
26
29
|
export interface XNative {
|
|
27
30
|
readonly searchRecentPosts: (input: {
|
|
28
31
|
readonly account: ConnectedAccountRef;
|
|
@@ -51,6 +54,18 @@ export interface XNative {
|
|
|
51
54
|
readonly postId: string;
|
|
52
55
|
readonly context: AdapterOperationContext;
|
|
53
56
|
}) => Promise<void>;
|
|
57
|
+
/** Uploads one MP4 Blob (up to 512 MiB) and waits for processing. Returns an attachable media ID. */
|
|
58
|
+
readonly uploadVideo: (input: {
|
|
59
|
+
readonly account: ConnectedAccountRef;
|
|
60
|
+
readonly video: Blob;
|
|
61
|
+
readonly context: AdapterOperationContext;
|
|
62
|
+
}) => Promise<XUploadedMedia>;
|
|
63
|
+
/** Uploads one GIF Blob (up to 15 MiB) and waits for processing. Returns an attachable media ID. */
|
|
64
|
+
readonly uploadGif: (input: {
|
|
65
|
+
readonly account: ConnectedAccountRef;
|
|
66
|
+
readonly gif: Blob;
|
|
67
|
+
readonly context: AdapterOperationContext;
|
|
68
|
+
}) => Promise<XUploadedMedia>;
|
|
54
69
|
readonly createPoll: (input: {
|
|
55
70
|
readonly account: ConnectedAccountRef;
|
|
56
71
|
readonly text: string;
|
package/dist/platforms/x.js
CHANGED
|
@@ -396,6 +396,267 @@ export function x(options) {
|
|
|
396
396
|
});
|
|
397
397
|
return string(data["id"]);
|
|
398
398
|
}
|
|
399
|
+
const xChunkBytes = 1024 * 1024;
|
|
400
|
+
const xMaxVideoBytes = 512 * 1024 * 1024;
|
|
401
|
+
const xMaxGifBytes = 15 * 1024 * 1024;
|
|
402
|
+
const xMaxStatusPolls = 30;
|
|
403
|
+
function chunkedCategory(media) {
|
|
404
|
+
if (media.source.kind !== "blob")
|
|
405
|
+
return undefined;
|
|
406
|
+
if (media.kind === "video" && media.mimeType === "video/mp4")
|
|
407
|
+
return "tweet_video";
|
|
408
|
+
if (media.kind === "image" && media.mimeType === "image/gif")
|
|
409
|
+
return "tweet_gif";
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
function chunkedLimits(media, category) {
|
|
413
|
+
const size = media.source.kind === "blob" ? media.source.blob.size : 0;
|
|
414
|
+
const maxBytes = category === "tweet_video" ? xMaxVideoBytes : xMaxGifBytes;
|
|
415
|
+
if (size <= 0 || size > maxBytes)
|
|
416
|
+
throw new SocialError({
|
|
417
|
+
code: "invalid_input",
|
|
418
|
+
operation: "media.upload",
|
|
419
|
+
message: category === "tweet_video"
|
|
420
|
+
? "X video uploads require a non-empty MP4 Blob up to 512 MiB."
|
|
421
|
+
: "X GIF uploads require a non-empty GIF Blob up to 15 MiB.",
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function mediaHttpError(error) {
|
|
425
|
+
if (error.kind === "cancelled")
|
|
426
|
+
return new SocialError({
|
|
427
|
+
code: "cancelled",
|
|
428
|
+
operation: "media.upload",
|
|
429
|
+
message: "X media upload was cancelled before completion.",
|
|
430
|
+
upstreamStatus: error.status,
|
|
431
|
+
});
|
|
432
|
+
if (error.kind === "timeout")
|
|
433
|
+
return new SocialError({
|
|
434
|
+
code: "timeout",
|
|
435
|
+
operation: "media.upload",
|
|
436
|
+
message: "X media upload exceeded its elapsed budget. Reconcile before retrying.",
|
|
437
|
+
upstreamStatus: error.status,
|
|
438
|
+
retryDisposition: { kind: "never" },
|
|
439
|
+
});
|
|
440
|
+
if (error.status === 401)
|
|
441
|
+
return new SocialError({
|
|
442
|
+
code: "reconnect_required",
|
|
443
|
+
operation: "media.upload",
|
|
444
|
+
message: "X rejected the media upload credentials. Reconnect before retrying.",
|
|
445
|
+
upstreamStatus: error.status,
|
|
446
|
+
retryDisposition: { kind: "after-reconnect" },
|
|
447
|
+
});
|
|
448
|
+
if (error.status === 429)
|
|
449
|
+
return new SocialError({
|
|
450
|
+
code: "rate_limited",
|
|
451
|
+
operation: "media.upload",
|
|
452
|
+
message: "X rate-limited the media upload.",
|
|
453
|
+
upstreamStatus: error.status,
|
|
454
|
+
retryDisposition: error.retryAfterMs === undefined
|
|
455
|
+
? { kind: "never" }
|
|
456
|
+
: { kind: "after-delay", delayMs: error.retryAfterMs },
|
|
457
|
+
});
|
|
458
|
+
if (error.status === 413)
|
|
459
|
+
return new SocialError({
|
|
460
|
+
code: "media_error",
|
|
461
|
+
operation: "media.upload",
|
|
462
|
+
message: "Media chunk rejected by X (payload too large).",
|
|
463
|
+
upstreamStatus: error.status,
|
|
464
|
+
retryDisposition: { kind: "never" },
|
|
465
|
+
});
|
|
466
|
+
return new SocialError({
|
|
467
|
+
code: "media_error",
|
|
468
|
+
operation: "media.upload",
|
|
469
|
+
message: error.message,
|
|
470
|
+
upstreamStatus: error.status,
|
|
471
|
+
retryDisposition: { kind: "never" },
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
async function chunkedPost(path, body, context) {
|
|
475
|
+
requireUserToken("media.upload");
|
|
476
|
+
let result;
|
|
477
|
+
try {
|
|
478
|
+
result = await http({
|
|
479
|
+
url: new URL(`https://api.x.com${path}`),
|
|
480
|
+
method: "POST",
|
|
481
|
+
headers: body === undefined
|
|
482
|
+
? { Authorization: `Bearer ${options.auth.accessToken}` }
|
|
483
|
+
: {
|
|
484
|
+
Authorization: `Bearer ${options.auth.accessToken}`,
|
|
485
|
+
"Content-Type": "application/json",
|
|
486
|
+
},
|
|
487
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- FINALIZE carries no body.
|
|
488
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
489
|
+
timeoutMs: remainingBudget(context),
|
|
490
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
491
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
if (!(error instanceof HttpError))
|
|
496
|
+
throw error;
|
|
497
|
+
throw mediaHttpError(error);
|
|
498
|
+
}
|
|
499
|
+
return object(object(result)["data"]);
|
|
500
|
+
}
|
|
501
|
+
async function appendChunk(mediaId, segmentIndex, chunk, context) {
|
|
502
|
+
requireUserToken("media.upload");
|
|
503
|
+
const body = new FormData();
|
|
504
|
+
body.set("segment_index", String(segmentIndex));
|
|
505
|
+
body.set("media", chunk, `chunk-${segmentIndex}`);
|
|
506
|
+
try {
|
|
507
|
+
await http({
|
|
508
|
+
url: new URL(`https://api.x.com/2/media/upload/${encodeURIComponent(mediaId)}/append`),
|
|
509
|
+
method: "POST",
|
|
510
|
+
headers: { Authorization: `Bearer ${options.auth.accessToken}` },
|
|
511
|
+
body,
|
|
512
|
+
timeoutMs: remainingBudget(context),
|
|
513
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
514
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
if (!(error instanceof HttpError))
|
|
519
|
+
throw error;
|
|
520
|
+
throw mediaHttpError(error);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async function readMediaStatus(mediaId, context) {
|
|
524
|
+
requireUserToken("media.upload");
|
|
525
|
+
let result;
|
|
526
|
+
try {
|
|
527
|
+
result = await http({
|
|
528
|
+
url: new URL(`https://api.x.com/2/media/upload?command=STATUS&media_id=${encodeURIComponent(mediaId)}`),
|
|
529
|
+
method: "GET",
|
|
530
|
+
headers: { Authorization: `Bearer ${options.auth.accessToken}` },
|
|
531
|
+
timeoutMs: remainingBudget(context),
|
|
532
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
533
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
if (!(error instanceof HttpError))
|
|
538
|
+
throw error;
|
|
539
|
+
throw mediaHttpError(error);
|
|
540
|
+
}
|
|
541
|
+
const data = object(object(result)["data"]);
|
|
542
|
+
const processing = data["processing_info"];
|
|
543
|
+
if (processing === undefined)
|
|
544
|
+
return { state: "succeeded", checkAfterSecs: 0 };
|
|
545
|
+
return processingState(object(processing));
|
|
546
|
+
}
|
|
547
|
+
function processingState(info) {
|
|
548
|
+
const checkAfter = optionalNumber(info["check_after_secs"]);
|
|
549
|
+
return {
|
|
550
|
+
state: optionalString(info["state"]) ?? "pending",
|
|
551
|
+
// X always sends check_after_secs while processing; a missing value must not spin the poll.
|
|
552
|
+
checkAfterSecs: checkAfter !== undefined && Number.isFinite(checkAfter) && checkAfter >= 0 ? checkAfter : 1,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function uploadCancelled() {
|
|
556
|
+
return new SocialError({
|
|
557
|
+
code: "cancelled",
|
|
558
|
+
operation: "media.upload",
|
|
559
|
+
message: "X media upload was cancelled before completion.",
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function throwIfUploadCancelled(context) {
|
|
563
|
+
if (context.signal?.aborted)
|
|
564
|
+
throw uploadCancelled();
|
|
565
|
+
}
|
|
566
|
+
/** Waits for X's processing hint without outliving the shared operation budget. */
|
|
567
|
+
function processingWait(milliseconds, context) {
|
|
568
|
+
throwIfUploadCancelled(context);
|
|
569
|
+
if (milliseconds <= 0)
|
|
570
|
+
return Promise.resolve();
|
|
571
|
+
if (milliseconds >= remainingBudget(context))
|
|
572
|
+
throw new SocialError({
|
|
573
|
+
code: "timeout",
|
|
574
|
+
operation: "media.upload",
|
|
575
|
+
message: "X media processing needs longer than the remaining elapsed budget. No post was created.",
|
|
576
|
+
retryDisposition: { kind: "never" },
|
|
577
|
+
});
|
|
578
|
+
return new Promise((resolve, reject) => {
|
|
579
|
+
const onAbort = () => {
|
|
580
|
+
clearTimeout(timer);
|
|
581
|
+
reject(uploadCancelled());
|
|
582
|
+
};
|
|
583
|
+
const timer = setTimeout(() => {
|
|
584
|
+
context.signal?.removeEventListener("abort", onAbort);
|
|
585
|
+
resolve();
|
|
586
|
+
}, milliseconds);
|
|
587
|
+
context.signal?.addEventListener("abort", onAbort, { once: true });
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
async function uploadVideoOrGif(media, context) {
|
|
591
|
+
try {
|
|
592
|
+
return await chunkedUpload(media, context);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
// Malformed upload responses fail before any post request, so they are definite failures.
|
|
596
|
+
if (error instanceof HttpError)
|
|
597
|
+
throw mediaHttpError(error);
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async function chunkedUpload(media, context) {
|
|
602
|
+
requireUserToken("media.upload");
|
|
603
|
+
const category = chunkedCategory(media);
|
|
604
|
+
if (category === undefined || media.source.kind !== "blob")
|
|
605
|
+
throw new SocialError({
|
|
606
|
+
code: "invalid_input",
|
|
607
|
+
operation: "media.upload",
|
|
608
|
+
message: "X chunked upload requires a video/mp4 or image/gif Blob.",
|
|
609
|
+
});
|
|
610
|
+
chunkedLimits(media, category);
|
|
611
|
+
const blob = media.source.blob;
|
|
612
|
+
const totalBytes = blob.size;
|
|
613
|
+
const initialized = await chunkedPost("/2/media/upload/initialize", {
|
|
614
|
+
media_category: category,
|
|
615
|
+
media_type: category === "tweet_video" ? "video/mp4" : "image/gif",
|
|
616
|
+
total_bytes: totalBytes,
|
|
617
|
+
}, context);
|
|
618
|
+
const mediaId = string(initialized["id"]);
|
|
619
|
+
const segmentCount = Math.ceil(totalBytes / xChunkBytes);
|
|
620
|
+
for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++) {
|
|
621
|
+
throwIfUploadCancelled(context);
|
|
622
|
+
const start = segmentIndex * xChunkBytes;
|
|
623
|
+
const end = Math.min(start + xChunkBytes, totalBytes);
|
|
624
|
+
const chunk = blob.slice(start, end, media.mimeType ?? "");
|
|
625
|
+
await appendChunk(mediaId, segmentIndex, chunk, context);
|
|
626
|
+
}
|
|
627
|
+
const finalized = await chunkedPost(`/2/media/upload/${encodeURIComponent(mediaId)}/finalize`, undefined, context);
|
|
628
|
+
const finalizedId = optionalString(finalized["id"]) ?? mediaId;
|
|
629
|
+
const processing = finalized["processing_info"];
|
|
630
|
+
if (processing === undefined)
|
|
631
|
+
return finalizedId;
|
|
632
|
+
let { state, checkAfterSecs } = processingState(object(processing));
|
|
633
|
+
for (let poll = 0;; poll++) {
|
|
634
|
+
if (state === "succeeded")
|
|
635
|
+
return finalizedId;
|
|
636
|
+
if (state === "failed")
|
|
637
|
+
throw new SocialError({
|
|
638
|
+
code: "media_error",
|
|
639
|
+
operation: "media.upload",
|
|
640
|
+
message: "X failed to process the uploaded media. No post was created.",
|
|
641
|
+
retryDisposition: { kind: "never" },
|
|
642
|
+
});
|
|
643
|
+
if (poll >= xMaxStatusPolls)
|
|
644
|
+
break;
|
|
645
|
+
await processingWait(checkAfterSecs * 1000, context);
|
|
646
|
+
({ state, checkAfterSecs } = await readMediaStatus(finalizedId, context));
|
|
647
|
+
}
|
|
648
|
+
throw new SocialError({
|
|
649
|
+
code: "timeout",
|
|
650
|
+
operation: "media.upload",
|
|
651
|
+
message: "X media processing did not complete in time. Reconcile before retrying.",
|
|
652
|
+
retryDisposition: { kind: "never" },
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
async function uploadXMedia(media, context) {
|
|
656
|
+
if (chunkedCategory(media) !== undefined)
|
|
657
|
+
return uploadVideoOrGif(media, context);
|
|
658
|
+
return uploadImage(media, context);
|
|
659
|
+
}
|
|
399
660
|
async function createPost(account, text, context, extra = {}, targetIndex = 0) {
|
|
400
661
|
authorize(account, context);
|
|
401
662
|
const result = object(await request("/2/tweets", context, { text, ...extra }));
|
|
@@ -435,9 +696,9 @@ export function x(options) {
|
|
|
435
696
|
platform: "x",
|
|
436
697
|
operation: "posts.publish",
|
|
437
698
|
availability: "available",
|
|
438
|
-
formats: ["text", "image"],
|
|
699
|
+
formats: ["text", "image", "video"],
|
|
439
700
|
requiredScopes: ["tweet.read", "tweet.write", "users.read", "media.write"],
|
|
440
|
-
notes: "User-context OAuth2 token; current X API access/billing required. Up to four static JPEG/PNG image Blobs, each at most 5 MiB
|
|
701
|
+
notes: "User-context OAuth2 token; current X API access/billing required. Up to four static JPEG/PNG image Blobs, each at most 5 MiB, or one MP4 video up to 512 MiB / one GIF up to 15 MiB via 1 MiB chunked upload with bounded processing poll. Post attach can still reject over-duration video with 403.",
|
|
441
702
|
},
|
|
442
703
|
...[
|
|
443
704
|
"accounts.read",
|
|
@@ -570,13 +831,17 @@ export function x(options) {
|
|
|
570
831
|
{
|
|
571
832
|
platform: "x",
|
|
572
833
|
operation: "media.video",
|
|
573
|
-
availability: "
|
|
834
|
+
availability: "available",
|
|
574
835
|
formats: ["video"],
|
|
836
|
+
requiredScopes: ["tweet.read", "tweet.write", "users.read", "media.write"],
|
|
837
|
+
notes: "MP4 Blob chunked INIT/APPEND/FINALIZE with 1 MiB segments and bounded STATUS poll honoring check_after_secs.",
|
|
575
838
|
},
|
|
576
839
|
{
|
|
577
840
|
platform: "x",
|
|
578
841
|
operation: "media.gif",
|
|
579
|
-
availability: "
|
|
842
|
+
availability: "available",
|
|
843
|
+
requiredScopes: ["tweet.read", "tweet.write", "users.read", "media.write"],
|
|
844
|
+
notes: "GIF Blob chunked upload; large GIFs process asynchronously before attach.",
|
|
580
845
|
},
|
|
581
846
|
{
|
|
582
847
|
platform: "x",
|
|
@@ -761,10 +1026,28 @@ export function x(options) {
|
|
|
761
1026
|
!["everyone", "following", "mentionedUsers"].includes(String(settings["replySettings"]))))
|
|
762
1027
|
fail("x.options", "Provide only a supported replySettings value.");
|
|
763
1028
|
const media = target.content.media ?? [];
|
|
764
|
-
|
|
1029
|
+
const hasChunked = media.some((item) => chunkedCategory(item) !== undefined);
|
|
1030
|
+
if (hasChunked && media.length !== 1)
|
|
1031
|
+
fail("x.media_count", "Attach a single video or GIF per post.");
|
|
1032
|
+
else if (!hasChunked && media.length > 4)
|
|
765
1033
|
fail("x.media_count", "Attach up to four images.");
|
|
766
1034
|
for (const item of media) {
|
|
767
|
-
|
|
1035
|
+
const category = chunkedCategory(item);
|
|
1036
|
+
if (category === "tweet_video") {
|
|
1037
|
+
if (item.source.kind !== "blob" || item.source.blob.size === 0)
|
|
1038
|
+
fail("x.video_size", "Video must be a non-empty MP4 Blob.");
|
|
1039
|
+
else if (item.source.blob.size > xMaxVideoBytes)
|
|
1040
|
+
fail("x.video_size", "Video exceeds the 512 MiB limit.");
|
|
1041
|
+
}
|
|
1042
|
+
else if (category === "tweet_gif") {
|
|
1043
|
+
if (item.source.kind !== "blob" || item.source.blob.size === 0)
|
|
1044
|
+
fail("x.gif_size", "GIF must be a non-empty Blob.");
|
|
1045
|
+
else if (item.source.blob.size > xMaxGifBytes)
|
|
1046
|
+
fail("x.gif_size", "GIF exceeds the 15 MiB limit.");
|
|
1047
|
+
}
|
|
1048
|
+
else if (item.kind === "video")
|
|
1049
|
+
fail("x.video", "X video uploads require a video/mp4 Blob.");
|
|
1050
|
+
else if (item.kind !== "image" ||
|
|
768
1051
|
item.source.kind !== "blob" ||
|
|
769
1052
|
!["image/jpeg", "image/png"].includes(item.mimeType ?? ""))
|
|
770
1053
|
fail("x.image", "This slice requires a JPEG or PNG image Blob.");
|
|
@@ -779,18 +1062,36 @@ export function x(options) {
|
|
|
779
1062
|
authorize(target.account, context);
|
|
780
1063
|
const ids = [];
|
|
781
1064
|
for (const media of target.content.media ?? [])
|
|
782
|
-
ids.push(await
|
|
1065
|
+
ids.push(await uploadXMedia(media, context));
|
|
783
1066
|
const settings = target.options === undefined ? {} : object(target.options);
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1067
|
+
const hasVideo = (target.content.media ?? []).some((media) => chunkedCategory(media) === "tweet_video");
|
|
1068
|
+
try {
|
|
1069
|
+
return await createPost(target.account, target.content.text ?? "", context, {
|
|
1070
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
1071
|
+
...(ids.length ? { media: { media_ids: ids } } : {}),
|
|
1072
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
1073
|
+
...(target.replyTo ? { reply: { in_reply_to_tweet_id: target.replyTo.postId } } : {}),
|
|
1074
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
1075
|
+
...(settings["replySettings"] && settings["replySettings"] !== "everyone"
|
|
1076
|
+
? { reply_settings: string(settings["replySettings"]) }
|
|
1077
|
+
: {}),
|
|
1078
|
+
}, target.targetIndex);
|
|
1079
|
+
}
|
|
1080
|
+
catch (error) {
|
|
1081
|
+
// X checks video duration only at attach time and answers 403. The transport drops the
|
|
1082
|
+
// response body, so the adapter cannot tell a duration limit from a missing permission.
|
|
1083
|
+
if (hasVideo && error instanceof SocialError && error.upstreamStatus === 403)
|
|
1084
|
+
throw new SocialError({
|
|
1085
|
+
code: "missing_permission",
|
|
1086
|
+
operation: error.operation,
|
|
1087
|
+
backend: error.backend,
|
|
1088
|
+
correlationId: error.correlationId,
|
|
1089
|
+
message: "X rejected the post with its attached video (HTTP 403). The video may exceed this account's duration limit, or the token may lack post permission. No post was created.",
|
|
1090
|
+
upstreamStatus: 403,
|
|
1091
|
+
retryDisposition: { kind: "never" },
|
|
1092
|
+
});
|
|
1093
|
+
throw error;
|
|
1094
|
+
}
|
|
794
1095
|
},
|
|
795
1096
|
async get(ref, context) {
|
|
796
1097
|
return publicFields(await readPost(ref, context), [
|
|
@@ -1003,6 +1304,24 @@ export function x(options) {
|
|
|
1003
1304
|
authorize(account, context);
|
|
1004
1305
|
await request(`/2/tweets/${encodeURIComponent(postId)}`, context, undefined, {}, "DELETE");
|
|
1005
1306
|
},
|
|
1307
|
+
async uploadVideo({ account, video, context }) {
|
|
1308
|
+
authorize(account, context);
|
|
1309
|
+
const media = {
|
|
1310
|
+
kind: "video",
|
|
1311
|
+
mimeType: "video/mp4",
|
|
1312
|
+
source: { kind: "blob", blob: video, fingerprint: "native-upload" },
|
|
1313
|
+
};
|
|
1314
|
+
return { mediaId: await uploadVideoOrGif(media, context) };
|
|
1315
|
+
},
|
|
1316
|
+
async uploadGif({ account, gif, context }) {
|
|
1317
|
+
authorize(account, context);
|
|
1318
|
+
const media = {
|
|
1319
|
+
kind: "image",
|
|
1320
|
+
mimeType: "image/gif",
|
|
1321
|
+
source: { kind: "blob", blob: gif, fingerprint: "native-upload" },
|
|
1322
|
+
};
|
|
1323
|
+
return { mediaId: await uploadVideoOrGif(media, context) };
|
|
1324
|
+
},
|
|
1006
1325
|
async createPoll({ account, text, options: pollOptions, durationMinutes, context }) {
|
|
1007
1326
|
authorize(account, context);
|
|
1008
1327
|
if (pollOptions.length < 2 ||
|