@@ -123,15 +123,13 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
123123 } ;
124124 }
125125
126- private async uploadFileToReddit ( accessToken : string , path : string ) {
126+ private async uploadFileToReddit ( accessToken : string , path : string ) : Promise < { url : string ; assetId ?: string } > {
127127 const mimeType = lookup ( path ) ;
128128 const formData = new FormData ( ) ;
129129 formData . append ( 'filepath' , path . split ( '/' ) . pop ( ) ) ;
130130 formData . append ( 'mimetype' , mimeType || 'application/octet-stream' ) ;
131131
132- const {
133- args : { action, fields } ,
134- } = await (
132+ const responseData = await (
135133 await this . fetch (
136134 'https://oauth.reddit.com/api/media/asset' ,
137135 {
@@ -147,6 +145,11 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
147145 )
148146 ) . json ( ) ;
149147
148+ const {
149+ args : { action, fields } ,
150+ asset,
151+ } = responseData ;
152+
150153 const { data } = await axios . get ( path , {
151154 responseType : 'arraybuffer' ,
152155 } ) ;
@@ -169,7 +172,12 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
169172 body : upload ,
170173 } ) ;
171174
172- return [ ...( await d . text ( ) ) . matchAll ( / < L o c a t i o n > ( .* ?) < \/ L o c a t i o n > / g) ] [ 0 ] [ 1 ] ;
175+ const url = [ ...( await d . text ( ) ) . matchAll ( / < L o c a t i o n > ( .* ?) < \/ L o c a t i o n > / g) ] [ 0 ] [ 1 ] ;
176+
177+ // Extract asset_id from the asset object if available
178+ const assetId = asset ?. asset_id || asset ?. id ;
179+
180+ return { url, assetId } ;
173181 }
174182
175183 async post (
@@ -181,53 +189,165 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
181189
182190 const valueArray : PostResponse [ ] = [ ] ;
183191 for ( const firstPostSettings of post . settings . subreddit ) {
184- const postData = {
192+ // Detect media type
193+ const hasVideo = post . media ?. some ( ( m ) => m . path . indexOf ( 'mp4' ) > - 1 ) === true ;
194+ const images = ( post . media || [ ] ) . filter ( ( m ) => m . path . indexOf ( 'mp4' ) === - 1 ) ;
195+
196+ // Determine kind based on media type
197+ let kind : string = firstPostSettings . value . type ;
198+ if ( firstPostSettings . value . type === 'media' ) {
199+ if ( hasVideo ) {
200+ kind = 'video' ;
201+ } else if ( images . length > 1 ) {
202+ kind = 'gallery' ;
203+ } else {
204+ kind = 'image' ;
205+ }
206+ }
207+
208+ // Build postData
209+ let postData : any = {
185210 api_type : 'json' ,
186211 title : firstPostSettings . value . title || '' ,
187- kind :
188- firstPostSettings . value . type === 'media'
189- ? post . media [ 0 ] . path . indexOf ( 'mp4' ) > - 1
190- ? 'video'
191- : 'image'
192- : firstPostSettings . value . type ,
212+ kind,
213+ text : post . message ,
214+ sr : firstPostSettings . value . subreddit ,
193215 ...( firstPostSettings . value . flair
194216 ? { flair_id : firstPostSettings . value . flair . id }
195217 : { } ) ,
196- ...( firstPostSettings . value . type === 'link'
197- ? {
198- url : firstPostSettings . value . url ,
199- }
200- : { } ) ,
201- ...( firstPostSettings . value . type === 'media'
202- ? {
203- url : await this . uploadFileToReddit (
204- accessToken ,
205- post . media [ 0 ] . path
206- ) ,
207- ...( post . media [ 0 ] . path . indexOf ( 'mp4' ) > - 1
208- ? {
209- video_poster_url : await this . uploadFileToReddit (
210- accessToken ,
211- post . media [ 0 ] . thumbnail
212- ) ,
213- }
214- : { } ) ,
215- }
216- : { } ) ,
217- text : post . message ,
218- sr : firstPostSettings . value . subreddit ,
219218 } ;
220219
221- const all = await (
222- await this . fetch ( 'https://oauth.reddit.com/api/submit' , {
223- method : 'POST' ,
224- headers : {
225- Authorization : `Bearer ${ accessToken } ` ,
226- 'Content-Type' : 'application/x-www-form-urlencoded' ,
227- } ,
228- body : new URLSearchParams ( postData ) ,
229- } )
230- ) . json ( ) ;
220+ // Add type-specific fields
221+ if ( firstPostSettings . value . type === 'link' ) {
222+ postData . url = firstPostSettings . value . url ;
223+ } else if ( firstPostSettings . value . type === 'media' ) {
224+ if ( kind === 'gallery' ) {
225+ // Gallery: upload all images and build items array
226+ const uploads : { url : string ; assetId ?: string } [ ] = [ ] ;
227+ for ( let i = 0 ; i < images . length ; i ++ ) {
228+ const uploaded = await this . uploadFileToReddit ( accessToken , images [ i ] . path ) ;
229+ uploads . push ( uploaded ) ;
230+
231+ // Add delay between Reddit API calls to respect rate limit (1 req/sec)
232+ if ( i < images . length - 1 ) {
233+ await timer ( 1000 ) ;
234+ }
235+ }
236+
237+ // Build items array with media_ids
238+ const items = uploads
239+ . filter ( ( u ) => u . assetId )
240+ . map ( ( u ) => ( { media_id : u . assetId } ) ) ;
241+
242+ // Reddit requires at least 2 items for a gallery
243+ if ( items . length >= 2 ) {
244+ postData . items = JSON . stringify ( items ) ;
245+ } else {
246+ // Fallback: if less than 2 asset IDs, use first image as single image
247+ console . warn ( `Reddit gallery: only ${ items . length } valid asset ID(s), falling back to single image` ) ;
248+ kind = 'image' ;
249+ postData . kind = 'image' ;
250+ postData . url = uploads [ 0 ] . url ;
251+ }
252+ } else if ( kind === 'video' ) {
253+ // Video: upload video and thumbnail
254+ const videoUpload = await this . uploadFileToReddit ( accessToken , post . media [ 0 ] . path ) ;
255+ postData . url = videoUpload . url ;
256+
257+ if ( post . media [ 0 ] . thumbnail ) {
258+ const thumbnailUpload = await this . uploadFileToReddit ( accessToken , post . media [ 0 ] . thumbnail ) ;
259+ postData . video_poster_url = thumbnailUpload . url ;
260+ }
261+ } else {
262+ // Single image
263+ const imageUpload = await this . uploadFileToReddit ( accessToken , post . media [ 0 ] . path ) ;
264+ postData . url = imageUpload . url ;
265+ }
266+ }
267+
268+ // Try to submit, with fallback for gallery
269+ let all : any ;
270+ try {
271+ all = await (
272+ await this . fetch ( 'https://oauth.reddit.com/api/submit' , {
273+ method : 'POST' ,
274+ headers : {
275+ Authorization : `Bearer ${ accessToken } ` ,
276+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
277+ } ,
278+ body : new URLSearchParams ( postData ) ,
279+ } )
280+ ) . json ( ) ;
281+
282+ // Check for gallery-specific errors and fallback to single image
283+ if (
284+ kind === 'gallery' &&
285+ all ?. json ?. errors ?. length > 0 &&
286+ ( all . json . errors . some ( ( e : any ) =>
287+ e [ 0 ] === 'INVALID_OPTION' ||
288+ e [ 0 ] === 'BAD_GALLERY' ||
289+ ( typeof e [ 1 ] === 'string' && e [ 1 ] . toLowerCase ( ) . includes ( 'gallery' ) )
290+ ) )
291+ ) {
292+ console . warn ( 'Reddit gallery not supported in this subreddit, falling back to single image' ) ;
293+
294+ // Fallback: post as single image using first image
295+ const firstImageUpload = await this . uploadFileToReddit ( accessToken , images [ 0 ] . path ) ;
296+ postData = {
297+ api_type : 'json' ,
298+ title : firstPostSettings . value . title || '' ,
299+ kind : 'image' ,
300+ url : firstImageUpload . url ,
301+ text : post . message ,
302+ sr : firstPostSettings . value . subreddit ,
303+ ...( firstPostSettings . value . flair
304+ ? { flair_id : firstPostSettings . value . flair . id }
305+ : { } ) ,
306+ } ;
307+
308+ all = await (
309+ await this . fetch ( 'https://oauth.reddit.com/api/submit' , {
310+ method : 'POST' ,
311+ headers : {
312+ Authorization : `Bearer ${ accessToken } ` ,
313+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
314+ } ,
315+ body : new URLSearchParams ( postData ) ,
316+ } )
317+ ) . json ( ) ;
318+ }
319+ } catch ( err ) {
320+ // If any error occurs during gallery submission, fallback to single image
321+ if ( kind === 'gallery' && images . length > 0 ) {
322+ console . warn ( 'Reddit gallery submission failed, falling back to single image:' , err ) ;
323+
324+ const firstImageUpload = await this . uploadFileToReddit ( accessToken , images [ 0 ] . path ) ;
325+ postData = {
326+ api_type : 'json' ,
327+ title : firstPostSettings . value . title || '' ,
328+ kind : 'image' ,
329+ url : firstImageUpload . url ,
330+ text : post . message ,
331+ sr : firstPostSettings . value . subreddit ,
332+ ...( firstPostSettings . value . flair
333+ ? { flair_id : firstPostSettings . value . flair . id }
334+ : { } ) ,
335+ } ;
336+
337+ all = await (
338+ await this . fetch ( 'https://oauth.reddit.com/api/submit' , {
339+ method : 'POST' ,
340+ headers : {
341+ Authorization : `Bearer ${ accessToken } ` ,
342+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
343+ } ,
344+ body : new URLSearchParams ( postData ) ,
345+ } )
346+ ) . json ( ) ;
347+ } else {
348+ throw err ;
349+ }
350+ }
231351
232352 const { id, name, url } = await new Promise < {
233353 id : string ;
@@ -238,6 +358,12 @@ export class RedditProvider extends SocialAbstract implements SocialProvider {
238358 res ( all . json . data ) ;
239359 }
240360
361+ // Guard against missing websocket_url
362+ if ( ! all ?. json ?. data ?. websocket_url ) {
363+ res ( { id : '' , name : '' , url : '' } ) ;
364+ return ;
365+ }
366+
241367 const ws = new WebSocket ( all . json . data . websocket_url ) ;
242368 ws . on ( 'message' , ( data : any ) => {
243369 setTimeout ( ( ) => {
0 commit comments