omnivore/packages/api/src/services/integrations/readwise.ts
2024-03-20 10:35:58 +08:00

131 lines
3.6 KiB
TypeScript

import axios from 'axios'
import { LibraryItem } from '../../entity/library_item'
import { logger } from '../../utils/logger'
import { getHighlightUrl } from '../highlights'
import { IntegrationClient } from './integration'
interface ReadwiseHighlight {
// The highlight text, (technically the only field required in a highlight object)
text: string
// The title of the page the highlight is on
title?: string
// The author of the page the highlight is on
author?: string
// The URL of the page image
image_url?: string
// The URL of the page
source_url?: string
// A meaningful unique identifier for your app
source_type?: string
// One of: books, articles, tweets or podcasts
category?: string
// Annotation note attached to the specific highlight
note?: string
// Highlight's location in the source text. Used to order the highlights
location?: number
// One of: page, order or time_offset
location_type?: string
// A datetime representing when the highlight was taken in the ISO 8601 format
highlighted_at?: string
// Unique url of the specific highlight
highlight_url?: string
}
export class ReadwiseClient implements IntegrationClient {
name = 'READWISE'
token: string
_headers = {
'Content-Type': 'application/json',
}
_axios = axios.create({
baseURL: 'https://readwise.io/api/v2',
timeout: 5000, // 5 seconds
})
constructor(token: string) {
this.token = token
}
accessToken = async (): Promise<string | null> => {
try {
const response = await this._axios.get('/auth', {
headers: {
...this._headers,
Authorization: `Token ${this.token}`,
},
})
return response.status === 204 ? this.token : null
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error(error.response)
} else {
logger.error(error)
}
return null
}
}
export = async (items: LibraryItem[]): Promise<boolean> => {
let result = true
const highlights = items.flatMap(this._itemToReadwiseHighlight)
// If there are no highlights, we will skip the sync
if (highlights.length > 0) {
result = await this._syncWithReadwise(highlights)
}
return result
}
auth = () => {
throw new Error('Method not implemented.')
}
private _itemToReadwiseHighlight = (
item: LibraryItem
): ReadwiseHighlight[] => {
const category = item.siteName === 'Twitter' ? 'tweets' : 'articles'
return item.highlights
?.map((highlight) => {
// filter out highlights that are not of type highlight or have no quote
if (highlight.highlightType !== 'HIGHLIGHT' || !highlight.quote) {
return undefined
}
return {
text: highlight.quote,
title: item.title,
author: item.author || undefined,
highlight_url: getHighlightUrl(item.slug, highlight.id),
highlighted_at: new Date(highlight.createdAt).toISOString(),
category,
image_url: item.thumbnail || undefined,
location_type: 'order',
note: highlight.annotation || undefined,
source_type: 'omnivore',
source_url: item.originalUrl,
}
})
.filter((highlight) => highlight !== undefined) as ReadwiseHighlight[]
}
private _syncWithReadwise = async (
highlights: ReadwiseHighlight[]
): Promise<boolean> => {
const response = await this._axios.post(
'/highlights',
{
highlights,
},
{
headers: {
...this._headers,
Authorization: `Token ${this.token}`,
},
}
)
return response.status === 200
}
}