@@ -13,7 +13,7 @@ const FEEDLY_API_BASE = "https://api.feedly.com";
1313// Feedly Read Later pages can vary by user id and legacy paths.
1414const READ_LATER_PATTERNS = [
1515 / h t t p s : \/ \/ f e e d l y \. c o m \/ i \/ b o a r d \/ c o n t e n t \/ u s e r \/ [ ^ / ] + \/ t a g \/ g l o b a l \. s a v e d / i,
16- / h t t p s : \/ \/ f e e d l y \. c o m \/ i \/ r e a d - l a t e r / i
16+ / h t t p s : \/ \/ f e e d l y \. c o m \/ i \/ r e a d - l a t e r / i,
1717] ;
1818const READ_LATER_PATH_HINTS = [ "/i/read-later" , "/i/board/content/user/" ] ;
1919const READ_LATER_CACHE_MS = 3000 ;
@@ -30,7 +30,7 @@ const TOOLBAR_BUTTON_SELECTOR = "button.EntryToolbar__button";
3030const READ_LATER_SELECTORS = [
3131 ".EntryMetadataReadLater a[role='button']" ,
3232 ".EntryMetadataReadLater button" ,
33- "a[role='button'] .InterestingMetadata__icon"
33+ "a[role='button'] .InterestingMetadata__icon" ,
3434] ;
3535const READ_LATER_LABELS = [ "read later" , "後で読む" , "あとで読む" ] ;
3636
@@ -116,8 +116,8 @@ async function feedlyApiRequest(endpoint, options = {}) {
116116 headers : {
117117 "Authorization" : `Bearer ${ token } ` ,
118118 "Content-Type" : "application/json" ,
119- ...options . headers
120- }
119+ ...options . headers ,
120+ } ,
121121 } ) ;
122122
123123 if ( ! response . ok ) {
@@ -130,7 +130,9 @@ async function feedlyApiRequest(endpoint, options = {}) {
130130 }
131131
132132 // DELETE requests may return empty body
133- if ( response . status === 204 || response . headers . get ( "content-length" ) === "0" ) {
133+ if (
134+ response . status === 204 || response . headers . get ( "content-length" ) === "0"
135+ ) {
134136 return { success : true } ;
135137 }
136138
@@ -160,8 +162,9 @@ function parseEntryItems(items) {
160162 }
161163 return items . map ( ( item ) => ( {
162164 id : item . id ,
163- url : item . alternate ?. [ 0 ] ?. href || item . canonicalUrl || item . originId || null ,
164- title : item . title || "Untitled"
165+ url : item . alternate ?. [ 0 ] ?. href || item . canonicalUrl || item . originId ||
166+ null ,
167+ title : item . title || "Untitled" ,
165168 } ) ) . filter ( ( item ) => item . url ) ;
166169}
167170
@@ -174,7 +177,7 @@ function parseEntryItems(items) {
174177async function fetchSavedEntriesViaAPI ( userId , count = 100 ) {
175178 const streamId = encodeURIComponent ( `user/${ userId } /tag/global.saved` ) ;
176179 const response = await feedlyApiRequest (
177- `/v3/streams/contents?streamId=${ streamId } &count=${ count } &ranked=newest`
180+ `/v3/streams/contents?streamId=${ streamId } &count=${ count } &ranked=newest` ,
178181 ) ;
179182 return parseEntryItems ( response . items ) ;
180183}
@@ -189,10 +192,19 @@ async function fetchAllSavedEntriesViaAPI(userId) {
189192 const allEntries = [ ] ;
190193 let continuation = null ;
191194 const PAGE_SIZE = 100 ;
195+ const MAX_PAGES = 1000 ; // Safety limit to prevent infinite loops
196+ let pageCount = 0 ;
192197
193198 do {
199+ if ( pageCount ++ >= MAX_PAGES ) {
200+ console . warn (
201+ "[Feedly Opener] Reached maximum page limit while fetching saved entries" ,
202+ ) ;
203+ break ;
204+ }
194205 const streamId = encodeURIComponent ( `user/${ userId } /tag/global.saved` ) ;
195- let url = `/v3/streams/contents?streamId=${ streamId } &count=${ PAGE_SIZE } &ranked=newest` ;
206+ let url =
207+ `/v3/streams/contents?streamId=${ streamId } &count=${ PAGE_SIZE } &ranked=newest` ;
196208 if ( continuation ) {
197209 url += `&continuation=${ encodeURIComponent ( continuation ) } ` ;
198210 }
@@ -225,13 +237,19 @@ async function unsaveEntriesViaAPI(userId, entryIds) {
225237 // Process DELETE requests in parallel batches to balance speed and rate limiting
226238 for ( let i = 0 ; i < entryIds . length ; i += BATCH_SIZE ) {
227239 const batch = entryIds . slice ( i , i + BATCH_SIZE ) ;
228- await Promise . all (
240+ const results = await Promise . all (
229241 batch . map ( ( entryId ) =>
230242 feedlyApiRequest ( `/v3/tags/${ tagId } /${ encodeURIComponent ( entryId ) } ` , {
231- method : "DELETE"
243+ method : "DELETE" ,
232244 } )
233- )
245+ ) ,
234246 ) ;
247+ const failures = results . filter ( ( res ) => ! res . success ) ;
248+ if ( failures . length > 0 ) {
249+ throw new Error (
250+ `Failed to unsave some entries via API: ${ failures . length } failures` ,
251+ ) ;
252+ }
235253 }
236254 return true ;
237255}
@@ -244,7 +262,9 @@ async function unsaveEntriesViaAPI(userId, entryIds) {
244262async function handleOpenViaAPI ( settings ) {
245263 const token = await getAccessToken ( ) ;
246264 if ( ! token ) {
247- throw new Error ( "No access token available. Please ensure you are logged into Feedly." ) ;
265+ throw new Error (
266+ "No access token available. Please ensure you are logged into Feedly." ,
267+ ) ;
248268 }
249269
250270 const userId = await getUserId ( ) ;
@@ -259,7 +279,7 @@ async function handleOpenViaAPI(settings) {
259279 ok : true ,
260280 urls : [ ] ,
261281 method : "api" ,
262- message : "No saved entries found"
282+ message : "No saved entries found" ,
263283 } ;
264284 }
265285
@@ -274,7 +294,7 @@ async function handleOpenViaAPI(settings) {
274294 return {
275295 ok : true ,
276296 urls : entriesToProcess . map ( ( e ) => e . url ) ,
277- method : "api"
297+ method : "api" ,
278298 } ;
279299}
280300
@@ -327,7 +347,7 @@ function toOpenableUrl(href) {
327347 let url ;
328348 try {
329349 url = new URL ( href , location . href ) ;
330- } catch ( error ) {
350+ } catch ( _error ) {
331351 return null ;
332352 }
333353
@@ -404,7 +424,8 @@ function isSavedButton(button) {
404424}
405425
406426function containsReadLaterText ( element ) {
407- const text = ( element . textContent || "" ) . replace ( / \s + / g, " " ) . trim ( ) . toLowerCase ( ) ;
427+ const text = ( element . textContent || "" ) . replace ( / \s + / g, " " ) . trim ( )
428+ . toLowerCase ( ) ;
408429 return READ_LATER_LABELS . some ( ( label ) => text . includes ( label ) ) ;
409430}
410431
@@ -478,8 +499,9 @@ async function getSavedEntriesWithUrls(settings) {
478499 const entries = getEntryElements ( ) ;
479500 const seen = new Set ( ) ;
480501 const results = [ ] ;
481- const limit =
482- settings . mode === "count" ? normalizeCount ( settings . count ) : Infinity ;
502+ const limit = settings . mode === "count"
503+ ? normalizeCount ( settings . count )
504+ : Infinity ;
483505
484506 for ( const entry of entries ) {
485507 if ( results . length >= limit ) {
@@ -528,15 +550,15 @@ function clickElement(element) {
528550 "mousedown" ,
529551 "pointerup" ,
530552 "mouseup" ,
531- "click"
553+ "click" ,
532554 ] ;
533555 for ( const type of mouseEvents ) {
534556 element . dispatchEvent (
535557 new MouseEvent ( type , {
536558 bubbles : true ,
537559 cancelable : true ,
538- view : window
539- } )
560+ view : window ,
561+ } ) ,
540562 ) ;
541563 }
542564}
@@ -556,7 +578,7 @@ async function loadAllEntries({ maxRounds, idleThreshold }) {
556578 for ( let round = 0 ; round < maxRounds ; round += 1 ) {
557579 scrollElement . scrollTo ( {
558580 top : scrollElement . scrollHeight ,
559- behavior : "auto"
581+ behavior : "auto" ,
560582 } ) ;
561583 await delay ( 800 ) ;
562584
@@ -621,7 +643,7 @@ async function handleOpenViaDOM(settings) {
621643 return {
622644 ok : true ,
623645 urls : selected . map ( ( item ) => item . url ) ,
624- method : "dom"
646+ method : "dom" ,
625647 } ;
626648}
627649
@@ -639,7 +661,10 @@ async function handleOpen(settings) {
639661 result = await handleOpenViaAPI ( settings ) ;
640662 } catch ( e ) {
641663 apiError = e ;
642- console . warn ( "[Feedly Opener] API operation failed, falling back to DOM:" , e . message ) ;
664+ console . warn (
665+ "[Feedly Opener] API operation failed, falling back to DOM:" ,
666+ e . message ,
667+ ) ;
643668 }
644669
645670 // Fallback to DOM-based approach if API failed
@@ -653,8 +678,9 @@ async function handleOpen(settings) {
653678 console . error ( "[Feedly Opener] DOM operation also failed:" , domError ) ;
654679 return {
655680 ok : false ,
656- error : `API error: ${ apiError ?. message || "unknown" } . DOM error: ${ domError . message } ` ,
657- method : "failed"
681+ error : `API error: ${ apiError ?. message || "unknown"
682+ } . DOM error: ${ domError . message } `,
683+ method : "failed" ,
658684 } ;
659685 }
660686 }
@@ -703,7 +729,8 @@ function isRecentlyReadLater() {
703729 if ( Date . now ( ) - lastReadLaterSeenAt > READ_LATER_CACHE_MS ) {
704730 return false ;
705731 }
706- return location . origin === "https://feedly.com" && lastReadLaterUrl . length > 0 ;
732+ return location . origin === "https://feedly.com" &&
733+ lastReadLaterUrl . length > 0 ;
707734}
708735
709736// =============================================================================
0 commit comments