diff --git a/packages/api/src/services/integrations.ts b/packages/api/src/services/integrations.ts index 01065f371..8cddb4398 100644 --- a/packages/api/src/services/integrations.ts +++ b/packages/api/src/services/integrations.ts @@ -1,6 +1,22 @@ import { IntegrationType } from '../generated/graphql' import { env } from '../env' import axios from 'axios' +import { wait } from '../utils/helpers' + +export interface ReadwiseHighlight { + text: string // The highlight text, (technically the only field required in a highlight object) + title?: string // The title of the page the highlight is on + author?: string // The author of the page the highlight is on + image_url?: string // The URL of the page image + source_url?: string // The URL of the page + source_type?: string // A meaningful unique identifier for your app + category?: string // One of: books, articles, tweets or podcasts + note?: string // Annotation note attached to the specific highlight + location?: number // Highlight's location in the source text. Used to order the highlights + location_type?: string // One of: page, order or time_offset + highlighted_at?: string // A datetime representing when the highlight was taken in the ISO 8601 format + highlight_url?: string // Unique url of the specific highlight +} const READWISE_API_URL = 'https://readwise.io/api/v2' @@ -18,10 +34,56 @@ export const validateToken = async ( const validateReadwiseToken = async (token: string): Promise => { const authUrl = `${env.readwise.apiUrl || READWISE_API_URL}/auth` - const response = await axios.get(authUrl, { - headers: { - Authorization: `Token ${token}`, - }, - }) - return response.status === 204 + try { + const response = await axios.get(authUrl, { + headers: { + Authorization: `Token ${token}`, + }, + }) + return response.status === 204 + } catch (error) { + console.log('error validating readwise token', error) + return false + } +} + +export const createHighlightsInReadwise = async ( + token: string, + highlights: ReadwiseHighlight[], + retryCount = 0 +): Promise => { + const url = `${env.readwise.apiUrl || READWISE_API_URL}/highlights` + try { + const response = await axios.post( + url, + { + highlights: highlights.map((highlight) => ({ + ...highlight, + source_type: 'omnivore', + })), + }, + { + headers: { + Authorization: `Token ${token}`, + ContentType: 'application/json', + }, + } + ) + return response.status === 200 + } catch (error) { + if ( + axios.isAxiosError(error) && + error.response?.status === 429 && + retryCount < 3 + ) { + console.log('Readwise API rate limit exceeded, retrying...') + // wait for Retry-After seconds in the header if rate limited + // max retry count is 3 + const retryAfter = error.response?.headers['Retry-After'] || '10' // default to 10 seconds + await wait(parseInt(retryAfter, 10) * 1000) + return createHighlightsInReadwise(token, highlights, retryCount + 1) + } + console.log('Error creating highlights in Readwise', error) + return false + } } diff --git a/packages/api/src/utils/helpers.ts b/packages/api/src/utils/helpers.ts index 70cdb1154..98a9de742 100644 --- a/packages/api/src/utils/helpers.ts +++ b/packages/api/src/utils/helpers.ts @@ -254,3 +254,9 @@ export const validateUuid = (str: string): boolean => { export const isString = (check: any): check is string => { return typeof check === 'string' || check instanceof String } + +export const wait = (ms: number): Promise => { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +}