mrgodhani/raven-reader

View on GitHub
src/services/inoreader.js

Summary

Maintainability
F
1 wk
Test Coverage
import dayjs from 'dayjs'
import db from './db.js'
import store from '../store'
import uuidstring from 'uuid-by-string'
import truncate from './truncate'
import * as database from '../db'

const TAGS = {
  READ_TAG: 'user/-/state/com.google/read',
  FAVOURITE_TAG: 'user/-/state/com.google/starred'
}

const chunk = (arr, chunkSize = 1, cache = []) => {
  const tmp = [...arr]
  if (chunkSize <= 0) return cache
  while (tmp.length) cache.push(tmp.splice(0, chunkSize))
  return cache
}

const flattenDeep = arr =>
  Array.isArray(arr)
    ? arr.reduce((a, b) => [...flattenDeep(a), ...flattenDeep(b)], [])
    : [arr]

function checkIsPodCast (post) {
  return typeof post !== 'undefined' &&
    post.length && post.type.indexOf('audio') !== -1
}

function getCoverImage (postContent) {
  const dom = new DOMParser()
  const doc = dom.parseFromString(postContent, 'text/html')
  const image = doc.querySelector('img')
  if (image !== null && typeof image.getAttribute('src') !== 'undefined' && image.getAttribute('src').startsWith('https://')) {
    return doc.querySelector('img').getAttribute('src')
  }
  return null
}

async function refreshToken (credsData) {
  try {
    window.log.info('Refreshing Inoreader token')
    const data = await window.inoreader.refresh('https://www.inoreader.com/oauth2/token', {
      client_id: process.env.VUE_APP_INOREADER_CLIENT_ID,
      client_secret: process.env.VUE_APP_INOREADER_CLIENT_SECRET,
      grant_type: 'refresh_token',
      refresh_token: credsData.refresh_token
    })
    const currentData = JSON.parse(JSON.stringify(credsData))
    currentData.expires_in = dayjs().add(data.expires_in).valueOf()
    currentData.access_token = data.access_token
    store.dispatch('setInoreader', JSON.stringify(currentData))
    return currentData
  } catch (e) {
    window.log.info(e)
  }
}

export default {
  async getUserInfo (credsData) {
    const data = await window.inoreader.fetch('https://www.inoreader.com/reader/api/0/user-info', {
      access_token: credsData.access_token
    })
    return data
  },
  checkToken (credsData) {
    const currentTime = dayjs().valueOf()
    return currentTime >= credsData.expires_in
  },
  async getSubscriptions (credsData) {
    try {
      const subscriptions = await window.inoreader.fetch('https://www.inoreader.com/reader/api/0/subscription/list', {
        access_token: credsData.access_token
      })
      return subscriptions.subscriptions
    } catch (e) {
      window.log.info(e)
    }
  },
  async getUnreadIds (credsData) {
    const ids = []
    let continuation = null
    let apiUrl
    try {
      do {
        if (continuation === null) {
          apiUrl = 'https://www.inoreader.com/reader/api/0/stream/items/ids?output=json&s=user/-/state/com.google/reading-list&xt=user/-/state/com.google/read&n=1000'
        } else {
          apiUrl = `https://www.inoreader.com/reader/api/0/stream/items/ids?output=json&s=user/-/state/com.google/reading-list&xt=user/-/state/com.google/read&n=1000&c=${continuation}`
        }
        const data = await window.inoreader.fetch(apiUrl, {
          access_token: credsData.access_token
        })
        if (data.itemRefs) {
          ids.push(...data.itemRefs.map(item => {
            return item.id
          }))
        }
        continuation = typeof data.continuation !== 'undefined' ? data.continuation : null
      }
      while (continuation !== null)
      return ids
    } catch (e) {
      window.log.info(e)
    }
  },
  async getStarredIds (credsData) {
    const ids = []
    let continuation = null
    let apiUrl
    try {
      do {
        if (continuation === null) {
          apiUrl = 'https://www.inoreader.com/reader/api/0/stream/items/ids?output=json&s=user/-/state/com.google/starred&n=1000'
        } else {
          apiUrl = `https://www.inoreader.com/reader/api/0/stream/items/ids?output=json&s=user/-/state/com.google/starred&n=1000&c=${continuation}`
        }
        const data = await window.inoreader.fetch(apiUrl, {
          access_token: credsData.access_token
        })
        if (data.itemRefs) {
          ids.push(...data.itemRefs.map(item => {
            return item.id
          }))
        }
        continuation = typeof data.continuation !== 'undefined' ? data.continuation : null
      }
      while (continuation !== null)
      return ids
    } catch (e) {
      window.log.info(e)
    }
  },
  async getEntries (credsData, ids) {
    const entries = []
    const chunks = chunk(ids, 250)
    try {
      for (let i = 0; i < chunks.length; i++) {
        const postData = new URLSearchParams()
        for (let k = 0; k < chunks[i].length; k++) {
          postData.append('i', chunks[i][k])
        }
        const data = await window.inoreader.post('https://www.inoreader.com/reader/api/0/stream/items/contents?output=json', postData.toString(), credsData)
        entries.push(...data.items)
      }
      return entries
    } catch (e) {
      window.log.info(e)
    }
  },
  async markItem (credsData, type, ids) {
    const postData = new URLSearchParams()
    let tokenData
    if (this.checkToken(credsData)) {
      tokenData = await refreshToken(credsData)
    } else {
      tokenData = credsData
    }
    for (let i = 0; i < ids.length; i++) {
      postData.append('i', ids[i])
    }
    switch (type) {
      case 'READ':
        postData.append('a', TAGS.READ_TAG)
        break
      case 'UNREAD':
        postData.append('r', TAGS.READ_TAG)
        break
      case 'FAVOURITE':
        postData.append('a', TAGS.FAVOURITE_TAG)
        break
      case 'UNFAVOURITE':
        postData.append('r', TAGS.FAVOURITE_TAG)
        break
    }
    return await window.inoreader.post('https://www.inoreader.com/reader/api/0/edit-tag', postData.toString(), tokenData)
  },
  async syncTags (categories) {
    const currentCategories = await db.fetchCategoriesBySource('inoreader')
    const dbFormat = Array.from(categories).map((item) => {
      return {
        id: uuidstring(item),
        type: 'category',
        source: 'inoreader',
        title: item
      }
    })
    const diff = currentCategories.filter(item => !categories.has(item.title))
    if (diff.length > 0) {
      db.deleteCategoryMulti(diff.map(item => item.title))
    }
    const tobeAdded = dbFormat.filter(x => !new Set(currentCategories.map(item => item.title)).has(x.title))
    if (dbFormat.length > 0) {
      db.addCategory(tobeAdded.map(item => database.categoryTable.createRow(item)))
    }
  },
  async syncArticles (credsData) {
    if (this.checkToken(credsData)) {
      credsData = await refreshToken(credsData)
    }
    let subscriptions = await this.getSubscriptions(credsData)
    const unreadList = await this.getUnreadIds(credsData)
    const starredList = await this.getStarredIds(credsData)
    const entriesId = new Set([...unreadList, ...starredList])
    const entries = await this.getEntries(credsData, Array.from(entriesId))
    const folders = subscriptions.map(item => {
      return item.categories.map(cat => cat.label)
    })
    this.syncTags(new Set(flattenDeep(folders)))
    if (subscriptions) {
      const currentSubscriptions = await db.fetchServicesFeeds('inoreader')
      const currentFeedUrls = JSON.parse(JSON.stringify(currentSubscriptions)).map((item) => {
        return item.xmlurl
      })
      const inoreaderSubscriptions = new Set(subscriptions.map((item) => {
        return item.url
      }))
      const diff = currentFeedUrls.filter(item => !inoreaderSubscriptions.has(item))
      if (diff.length > 0) {
        const deleteFeed = currentSubscriptions.filter((x) => diff.includes(x.xmlurl))
        db.fetchArticlesByFeedMulti(deleteFeed.map(item => item.uuid))
        db.deleteFeedMulti(deleteFeed.map(item => item.uuid))
      }
      const transformedSubscriptions = subscriptions.map((item) => {
        return {
          id: uuidstring(item.url),
          uuid: uuidstring(item.url),
          link: item.htmlUrl,
          xmlurl: item.url,
          title: item.title,
          favicon: `https://www.google.com/s2/favicons?domain=${item.htmlUrl}`,
          description: null,
          category: item.categories.length > 0 ? item.categories[0].label : null,
          source: 'inoreader',
          source_id: item.id
        }
      })
      const addedFeeds = db.addFeed(transformedSubscriptions.map(item => database.feedTable.createRow(item)))
      return addedFeeds.then((res) => {
        const subscriptAdded = res
        subscriptions = subscriptions.map((item) => {
          const addedSubscription = subscriptAdded.filter(feed => feed.source_id === item.id)
          db.updateArticleCategoryFeed(addedSubscription[0].uuid, addedSubscription[0].category)
          item.feed_uuid = addedSubscription[0].uuid
          return item
        })
        const readTag = `user/${credsData.user.userId}/state/com.google/read`
        const favouriteTag = `user/${credsData.user.userId}/state/com.google/starred`
        const transformedEntries = JSON.parse(JSON.stringify(entries)).filter((item) => {
          const feed = subscriptions.filter(feed => feed.id === item.origin.streamId)[0]
          return typeof feed !== 'undefined'
        }).map((item) => {
          const itemId = item.id.split('/')
          const last = itemId[itemId.length - 1]
          const i = BigInt('0x' + last)
          const id = BigInt.asIntN(64, i).toString()
          const isMedia = item.alternate && (item.canonical[0].href.includes('youtube') || item.canonical[0].href.includes('vimeo'))
          const isPodcast = item.enclosure ? checkIsPodCast(item.enclosure[0]) : false
          const feed = subscriptions.filter(feed => feed.id === item.origin.streamId)[0]
          return {
            id: isPodcast ? uuidstring(item.enclosure[0].href) : uuidstring(item.canonical[0].href),
            uuid: isPodcast ? uuidstring(item.enclosure[0].href) : uuidstring(item.canonical[0].href),
            title: item.title,
            author: item.author,
            link: item.canonical[0].href,
            cover: getCoverImage(item.summary.content),
            content: item.summary.content,
            contentSnippet: truncate(item.summary.content.replace(/(<([^>]+)>)/gi, ''), 100),
            favourite: item.categories.includes(favouriteTag),
            read: item.categories.includes(readTag),
            keep_read: null,
            pubDate: item.published,
            offline: false,
            media: isMedia
              ? {
                  url: item.canonical[0].href,
                  description: item.summary.content.replace(/(<([^>]+)>)/gi, ''),
                  title: item.title
                }
              : null,
            podcast: isPodcast,
            enclosure: isPodcast
              ? {
                  type: item.enclosure[0].type,
                  url: item.enclosure[0].href,
                  length: item.enclosure[0].length
                }
              : null,
            itunes: item.itunes || null,
            played: false,
            publishUnix: item.published,
            feed_uuid: feed.feed_uuid,
            category: feed.categories.length > 0 ? feed.categories[0].label : null,
            source: 'inoreader',
            source_id: id
          }
        })
        const currentArticles = db.fetchServicesArticles('inoreader')
        const articles = db.addArticles(transformedEntries.map(item => database.articleTable.createRow(item)))
        currentArticles.then((res) => {
          const markRead = res.filter(item => !unreadList.includes(item.articles.source_id)).map(item => item.articles.uuid)
          const markUnFavourite = res.filter(item => !starredList.includes(item.articles.source_id)).map(item => item.articles.uuid)
          const markFavourite = res.filter(item => starredList.includes(item.articles.source_id)).map(item => item.articles.uuid)
          db.markAllRead(markRead)
          db.markBulkFavourite(markFavourite)
          db.markBulkUnFavourite(markUnFavourite)
        })
        store.dispatch('setInoreaderLastFetched', dayjs().toISOString())
        return articles
      })
    }
  }
}