diff --git a/packages/api/src/services/integrations/integration.ts b/packages/api/src/services/integrations/integration.ts index 13b61e82a..44c95e680 100644 --- a/packages/api/src/services/integrations/integration.ts +++ b/packages/api/src/services/integrations/integration.ts @@ -20,7 +20,7 @@ export interface RetrieveRequest { export interface IntegrationClient { name: string - _token: string + token: string accessToken(): Promise diff --git a/packages/api/src/services/integrations/notion.ts b/packages/api/src/services/integrations/notion.ts index f60b4e34b..21770fdc4 100644 --- a/packages/api/src/services/integrations/notion.ts +++ b/packages/api/src/services/integrations/notion.ts @@ -5,6 +5,7 @@ import { Integration } from '../../entity/integration' import { LibraryItem } from '../../entity/library_item' import { env } from '../../env' import { Merge } from '../../util' +import { highlightUrl } from '../../utils/helpers' import { logger } from '../../utils/logger' import { IntegrationClient } from './integration' @@ -91,35 +92,39 @@ interface NotionPage { }> } +type Property = 'highlights' | 'labels' | 'notes' + interface Settings { parentPageId: string parentDatabaseId: string + properties: Property[] } export class NotionClient implements IntegrationClient { name = 'NOTION' - _headers = { + token: string + + private headers = { 'Content-Type': 'application/json', Accept: 'application/json', 'Notion-Version': '2022-06-28', } - _timeout = 5000 // 5 seconds - _axios = axios.create({ + private timeout = 5000 // 5 seconds + private axiosInstance = axios.create({ baseURL: 'https://api.notion.com/v1', - timeout: this._timeout, + timeout: this.timeout, }) - _token: string - _client: Client - _integrationData?: Merge + private client: Client + private integrationData?: Merge constructor(token: string, integration?: Integration) { - this._token = token - this._client = new Client({ + this.token = token + this.client = new Client({ auth: token, - timeoutMs: this._timeout, + timeoutMs: this.timeout, }) - this._integrationData = integration + this.integrationData = integration } accessToken = async (): Promise => { @@ -129,16 +134,16 @@ export class NotionClient implements IntegrationClient { `${env.notion.clientId}:${env.notion.clientSecret}` ).toString('base64') - const response = await this._axios.post<{ access_token: string }>( + const response = await this.axiosInstance.post<{ access_token: string }>( '/oauth/token', { grant_type: 'authorization_code', - code: this._token, + code: this.token, redirect_uri: `${env.client.url}/settings/integrations`, }, { headers: { - ...this._headers, + ...this.headers, Authorization: `Basic ${encoded}`, }, } @@ -160,90 +165,101 @@ export class NotionClient implements IntegrationClient { private itemToNotionPage = ( item: LibraryItem, - databaseId: string - ): NotionPage => ({ - parent: { - database_id: databaseId, - }, - icon: item.siteIcon - ? { - external: { - url: item.siteIcon, - }, - } - : undefined, - cover: item.thumbnail - ? { - external: { - url: item.thumbnail, - }, - } - : undefined, - properties: { - Title: { - title: [ - { - text: { - content: item.title, + settings: Settings + ): NotionPage => { + return { + parent: { + database_id: settings.parentDatabaseId, + }, + icon: item.siteIcon + ? { + external: { + url: item.siteIcon, }, - }, - ], - }, - Author: { - rich_text: [ - { - text: { - content: item.author || 'unknown', - }, - }, - ], - }, - 'Original URL': { - url: item.originalUrl, - }, - 'Omnivore URL': { - url: `${env.client.url}/me/${item.slug}`, - }, - Tags: item.labels - ? { multi_select: item.labels.map((label) => ({ name: label.name })) } + } : undefined, - }, - children: item.highlights - ? item.highlights.map((highlight) => ({ - type: 'paragraph', - paragraph: { - rich_text: [ - { - text: { - content: highlight.quote || '', - link: { - url: `${env.client.url}/me/${item.slug}#${highlight.id}`, + cover: item.thumbnail + ? { + external: { + url: item.thumbnail, + }, + } + : undefined, + properties: { + Title: { + title: [ + { + text: { + content: item.title, + }, + }, + ], + }, + Author: { + rich_text: [ + { + text: { + content: item.author || 'unknown', + }, + }, + ], + }, + 'Original URL': { + url: item.originalUrl, + }, + 'Omnivore URL': { + url: `${env.client.url}/me/${item.slug}`, + }, + Tags: + item.labels && settings.properties.includes('labels') + ? { + multi_select: item.labels.map((label) => ({ + name: label.name, + })), + } + : undefined, + }, + children: item.highlights + ? item.highlights.map((highlight) => ({ + type: 'paragraph', + paragraph: { + rich_text: [ + { + text: { + content: settings.properties.includes('highlights') + ? highlight.quote || '' + : '', + link: { + url: highlightUrl(item.slug, highlight.id), + }, + }, + annotations: { + color: highlight.color as AnnotationColor, }, }, - annotations: { - color: highlight.color as AnnotationColor, + { + text: { + content: settings.properties.includes('notes') + ? `\n${highlight.annotation || ''}` + : '', + }, + annotations: { + italic: true, + }, }, - }, - { - text: { - content: `\n${highlight.annotation || ''}`, - }, - annotations: { - italic: true, - }, - }, - ], - }, - })) - : undefined, - }) + ], + }, + })) + : undefined, + } + } private createPage = async (page: NotionPage) => { - await this._client.pages.create(page) + await this.client.pages.create(page) } private findPage = async (url: string, databaseId: string) => { - const response = await this._client.databases.query({ + const response = await this.client.databases.query({ database_id: databaseId, page_size: 1, filter: { @@ -261,21 +277,22 @@ export class NotionClient implements IntegrationClient { } export = async (items: LibraryItem[]): Promise => { - if (!this._integrationData || !this._integrationData.settings) { + const settings = this.integrationData?.settings + if (!this.integrationData || !settings) { logger.error('Notion integration data not found') return false } - const pageId = this._integrationData.settings.parentPageId + const pageId = settings.parentPageId if (!pageId) { logger.error('Notion parent page id not found') return false } - let databaseId = this._integrationData.settings.parentDatabaseId + let databaseId = settings.parentDatabaseId if (!databaseId) { // create a database for the items - const database = await this._client.databases.create({ + const database = await this.client.databases.create({ parent: { page_id: pageId, }, @@ -315,33 +332,33 @@ export class NotionClient implements IntegrationClient { // save the database id databaseId = database.id await updateIntegration( - this._integrationData.id, + this.integrationData.id, { settings: { - ...this._integrationData.settings, + ...this.integrationData.settings, parentDatabaseId: databaseId, }, }, - this._integrationData.user.id + this.integrationData.user.id ) } await Promise.all( items.map(async (item) => { - const notionPage = this.itemToNotionPage(item, databaseId) + const notionPage = this.itemToNotionPage(item, settings) const url = notionPage.properties['Omnivore URL'].url const existingPage = await this.findPage(url, databaseId) if (existingPage) { // update the page - await this._client.pages.update({ + await this.client.pages.update({ page_id: existingPage.id, properties: notionPage.properties, }) // append the children if (notionPage.children) { - await this._client.blocks.children.append({ + await this.client.blocks.children.append({ block_id: existingPage.id, children: notionPage.children, }) diff --git a/packages/api/src/services/integrations/pocket.ts b/packages/api/src/services/integrations/pocket.ts index 20f4c212c..9945c7a6e 100644 --- a/packages/api/src/services/integrations/pocket.ts +++ b/packages/api/src/services/integrations/pocket.ts @@ -5,7 +5,7 @@ import { IntegrationClient } from './integration' export class PocketClient implements IntegrationClient { name = 'POCKET' - _token: string + token: string _axios = axios.create({ baseURL: 'https://getpocket.com/v3', headers: { @@ -16,7 +16,7 @@ export class PocketClient implements IntegrationClient { }) constructor(token: string) { - this._token = token + this.token = token } accessToken = async (): Promise => { @@ -25,7 +25,7 @@ export class PocketClient implements IntegrationClient { '/oauth/authorize', { consumer_key: env.pocket.consumerKey, - code: this._token, + code: this.token, } ) return response.data.access_token diff --git a/packages/api/src/services/integrations/readwise.ts b/packages/api/src/services/integrations/readwise.ts index 25a61f438..dfc5b43db 100644 --- a/packages/api/src/services/integrations/readwise.ts +++ b/packages/api/src/services/integrations/readwise.ts @@ -33,6 +33,8 @@ interface ReadwiseHighlight { export class ReadwiseClient implements IntegrationClient { name = 'READWISE' + token: string + _headers = { 'Content-Type': 'application/json', } @@ -40,10 +42,9 @@ export class ReadwiseClient implements IntegrationClient { baseURL: 'https://readwise.io/api/v2', timeout: 5000, // 5 seconds }) - _token: string constructor(token: string) { - this._token = token + this.token = token } accessToken = async (): Promise => { @@ -51,10 +52,10 @@ export class ReadwiseClient implements IntegrationClient { const response = await this._axios.get('/auth', { headers: { ...this._headers, - Authorization: `Token ${this._token}`, + Authorization: `Token ${this.token}`, }, }) - return response.status === 204 ? this._token : null + return response.status === 204 ? this.token : null } catch (error) { if (axios.isAxiosError(error)) { logger.error(error.response) @@ -121,7 +122,7 @@ export class ReadwiseClient implements IntegrationClient { { headers: { ...this._headers, - Authorization: `Token ${this._token}`, + Authorization: `Token ${this.token}`, }, } )