@@ -23,6 +23,7 @@ import type {
2323} from '../types' ;
2424import TwitterApiv2LabsReadWrite from '../v2-labs/client.v2.labs.write' ;
2525import { CreateDMConversationParams , PostDMInConversationParams , PostDMInConversationResult } from '../types/v2/dm.v2.types' ;
26+ import { MediaV2UploadAppendParams , MediaV2UploadFinalizeParams , MediaV2UploadInitParams , MediaV2UploadResponse } from '../types/v2/media.v2.types' ;
2627
2728/**
2829 * Base Twitter v2 client with read/write rights.
@@ -121,6 +122,85 @@ export default class TwitterApiv2ReadWrite extends TwitterApiv2ReadOnly {
121122 return this . post < TweetV2PostTweetResult > ( 'tweets' , payload ) ;
122123 }
123124
125+ /**
126+ * Uploads media to Twitter using chunked upload.
127+ * https://docs.x.com/x-api/media/media-upload
128+ *
129+ * @param media The media buffer to upload
130+ * @param options Upload options including media type and category
131+ * @param chunkSize Size of each chunk in bytes (default: 1MB)
132+ * @returns The media ID of the uploaded media
133+ */
134+ public async uploadMedia (
135+ media : Buffer ,
136+ options : { media_type : string ; media_category ?: string } ,
137+ chunkSize : number = 1024 * 1024
138+ ) : Promise < string > {
139+ const initArguments : MediaV2UploadInitParams = {
140+ command : 'INIT' ,
141+ media_type : options . media_type ,
142+ total_bytes : media . length ,
143+ media_category : options . media_category ,
144+ } ;
145+
146+ const initResponse = await this . post < MediaV2UploadResponse > ( 'media/upload' , initArguments ) ;
147+ const mediaId = initResponse . data . id ;
148+
149+ const chunks = Math . ceil ( media . length / chunkSize ) ;
150+ for ( let i = 0 ; i < chunks ; i ++ ) {
151+ const start = i * chunkSize ;
152+ const end = Math . min ( start + chunkSize , media . length ) ;
153+ const mediaChunk = Uint8Array . prototype . slice . call ( media , start , end ) ;
154+ const chunkedBuffer = Buffer . from ( mediaChunk ) ;
155+
156+ const appendArguments : MediaV2UploadAppendParams = {
157+ command : 'APPEND' ,
158+ media_id : mediaId ,
159+ segment_index : i ,
160+ media : chunkedBuffer ,
161+ } ;
162+
163+ await this . post ( 'media/upload' , appendArguments ) ;
164+ }
165+
166+ const finalizeArguments : MediaV2UploadFinalizeParams = {
167+ command : 'FINALIZE' ,
168+ media_id : mediaId ,
169+ } ;
170+
171+ const finalizeResponse = await this . post < MediaV2UploadResponse > ( 'media/upload' , finalizeArguments ) ;
172+ if ( finalizeResponse . data . processing_info ) {
173+ await this . waitForMediaProcessing ( mediaId ) ;
174+ }
175+
176+ return mediaId ;
177+ }
178+
179+ private async waitForMediaProcessing ( mediaId : string ) : Promise < void > {
180+ const response = await this . get < MediaV2UploadResponse > ( 'media/upload' , {
181+ command : 'STATUS' ,
182+ media_id : mediaId ,
183+ } ) ;
184+
185+ const info = response . data . processing_info ;
186+ if ( ! info ) return ;
187+
188+ switch ( info . state ) {
189+ case 'succeeded' :
190+ return ;
191+ case 'failed' :
192+ throw new Error ( `Media processing failed: ${ info . error ?. message } ` ) ;
193+ case 'pending' :
194+ case 'in_progress' : {
195+ const waitTime = info ?. check_after_secs ;
196+ if ( waitTime && waitTime > 0 ) {
197+ await new Promise ( resolve => setTimeout ( resolve , waitTime * 1000 ) ) ;
198+ await this . waitForMediaProcessing ( mediaId ) ;
199+ }
200+ }
201+ }
202+ }
203+
124204 /**
125205 * Reply to a Tweet on behalf of an authenticated user.
126206 * https://developer.x.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/post-tweets
0 commit comments