integrate with notion api client
This commit is contained in:
@ -24,6 +24,7 @@
|
||||
"@google-cloud/tasks": "^4.0.0",
|
||||
"@graphql-tools/utils": "^9.1.1",
|
||||
"@langchain/openai": "^0.0.14",
|
||||
"@notionhq/client": "^2.2.14",
|
||||
"@omnivore/content-handler": "1.0.0",
|
||||
"@omnivore/liqe": "1.0.0",
|
||||
"@omnivore/readability": "1.0.0",
|
||||
|
||||
@ -44,9 +44,9 @@ export const exportItem = async (jobData: ExportItemJobData) => {
|
||||
}
|
||||
logger.info('exporting item...', logObject)
|
||||
|
||||
const client = getIntegrationClient(integration.name)
|
||||
const client = getIntegrationClient(integration.name, integration.token)
|
||||
|
||||
const synced = await client.export(integration.token, libraryItems)
|
||||
const synced = await client.export(libraryItems)
|
||||
if (!synced) {
|
||||
logger.error('failed to export item', logObject)
|
||||
return false
|
||||
|
||||
@ -69,9 +69,9 @@ export const setIntegrationResolver = authorized<
|
||||
integrationToSave.taskName = existingIntegration.taskName
|
||||
} else {
|
||||
// Create
|
||||
const integrationService = getIntegrationClient(input.name)
|
||||
const integrationService = getIntegrationClient(input.name, input.token)
|
||||
// authorize and get access token
|
||||
const token = await integrationService.accessToken(input.token)
|
||||
const token = await integrationService.accessToken()
|
||||
if (!token) {
|
||||
return {
|
||||
errorCodes: [SetIntegrationErrorCode.InvalidToken],
|
||||
|
||||
@ -21,7 +21,7 @@ export function integrationRouter() {
|
||||
return res.status(401).send('UNAUTHORIZED')
|
||||
}
|
||||
|
||||
const integrationClient = getIntegrationClient(req.params.name)
|
||||
const integrationClient = getIntegrationClient(req.params.name, '')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const state = req.body.state as string
|
||||
|
||||
@ -6,20 +6,20 @@ import { NotionClient } from './notion'
|
||||
import { PocketClient } from './pocket'
|
||||
import { ReadwiseClient } from './readwise'
|
||||
|
||||
const integrations: IntegrationClient[] = [
|
||||
new ReadwiseClient(),
|
||||
new PocketClient(),
|
||||
new NotionClient(),
|
||||
]
|
||||
|
||||
export const getIntegrationClient = (name: string): IntegrationClient => {
|
||||
const service = integrations.find(
|
||||
(s) => s.name.toLowerCase() === name.toLowerCase()
|
||||
)
|
||||
if (!service) {
|
||||
throw new Error(`Integration client not found: ${name}`)
|
||||
export const getIntegrationClient = (
|
||||
name: string,
|
||||
token: string
|
||||
): IntegrationClient => {
|
||||
switch (name.toLowerCase()) {
|
||||
case 'readwise':
|
||||
return new ReadwiseClient(token)
|
||||
case 'pocket':
|
||||
return new PocketClient(token)
|
||||
case 'notion':
|
||||
return new NotionClient(token)
|
||||
default:
|
||||
throw new Error(`Integration client not found: ${name}`)
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
export const deleteIntegrations = async (
|
||||
|
||||
@ -20,11 +20,11 @@ export interface RetrieveRequest {
|
||||
|
||||
export interface IntegrationClient {
|
||||
name: string
|
||||
apiUrl: string
|
||||
_token: string
|
||||
|
||||
accessToken(token: string): Promise<string | null>
|
||||
accessToken(): Promise<string | null>
|
||||
|
||||
auth(state: string): Promise<string>
|
||||
|
||||
export(token: string, items: LibraryItem[]): Promise<boolean>
|
||||
export(items: LibraryItem[]): Promise<boolean>
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Client } from '@notionhq/client'
|
||||
import axios from 'axios'
|
||||
import { LibraryItem } from '../../entity/library_item'
|
||||
import { env } from '../../env'
|
||||
@ -6,7 +7,7 @@ import { IntegrationClient } from './integration'
|
||||
|
||||
interface NotionPage {
|
||||
parent: {
|
||||
database_id: string
|
||||
page_id: string
|
||||
}
|
||||
cover?: {
|
||||
external: {
|
||||
@ -14,55 +15,56 @@ interface NotionPage {
|
||||
}
|
||||
}
|
||||
properties: {
|
||||
Name: {
|
||||
title: Array<{
|
||||
text: {
|
||||
content: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
URL: {
|
||||
url: string
|
||||
}
|
||||
Tags: {
|
||||
multi_select: Array<{
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
title: Array<{
|
||||
text: {
|
||||
content: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export class NotionClient implements IntegrationClient {
|
||||
name = 'NOTION'
|
||||
apiUrl = 'https://api.notion.com/v1'
|
||||
headers = {
|
||||
_headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'Notion-Version': '2022-06-28',
|
||||
}
|
||||
timeout = 5000 // 5 seconds
|
||||
_timeout = 5000 // 5 seconds
|
||||
_axios = axios.create({
|
||||
baseURL: 'https://api.notion.com/v1',
|
||||
timeout: this._timeout,
|
||||
})
|
||||
_token: string
|
||||
_client: Client
|
||||
|
||||
accessToken = async (code: string): Promise<string | null> => {
|
||||
const authUrl = `${this.apiUrl}/oauth/token`
|
||||
constructor(token: string) {
|
||||
this._token = token
|
||||
this._client = new Client({
|
||||
auth: token,
|
||||
timeoutMs: this._timeout,
|
||||
})
|
||||
}
|
||||
|
||||
accessToken = async (): Promise<string | null> => {
|
||||
try {
|
||||
// encode in base 64
|
||||
const encoded = Buffer.from(
|
||||
`${env.notion.clientId}:${env.notion.clientSecret}`
|
||||
).toString('base64')
|
||||
|
||||
const response = await axios.post<{ access_token: string }>(
|
||||
authUrl,
|
||||
const response = await this._axios.post<{ access_token: string }>(
|
||||
'/oauth/token',
|
||||
{
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
code: this._token,
|
||||
redirect_uri: `${env.client.url}/settings/integrations`,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Basic ${encoded}`,
|
||||
...this.headers,
|
||||
...this._headers,
|
||||
Authorization: `Basic ${encoded}`,
|
||||
},
|
||||
timeout: this.timeout,
|
||||
}
|
||||
)
|
||||
return response.data.access_token
|
||||
@ -83,7 +85,7 @@ export class NotionClient implements IntegrationClient {
|
||||
private _itemToNotionPage = (item: LibraryItem): NotionPage => {
|
||||
return {
|
||||
parent: {
|
||||
database_id: item.id,
|
||||
page_id: '83a3f627ab9e44ac83fe657141aec615',
|
||||
},
|
||||
cover: item.thumbnail
|
||||
? {
|
||||
@ -93,49 +95,28 @@ export class NotionClient implements IntegrationClient {
|
||||
}
|
||||
: undefined,
|
||||
properties: {
|
||||
Name: {
|
||||
title: [
|
||||
{
|
||||
text: {
|
||||
content: item.title,
|
||||
},
|
||||
title: [
|
||||
{
|
||||
text: {
|
||||
content: item.title,
|
||||
},
|
||||
],
|
||||
},
|
||||
URL: {
|
||||
url: item.originalUrl,
|
||||
},
|
||||
Tags: {
|
||||
multi_select:
|
||||
item.labels?.map((label) => {
|
||||
return {
|
||||
name: label.name,
|
||||
}
|
||||
}) || [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export = async (token: string, items: LibraryItem[]): Promise<boolean> => {
|
||||
const url = `${this.apiUrl}/pages`
|
||||
const page = this._itemToNotionPage(items[0])
|
||||
try {
|
||||
const response = await axios.post(url, page, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...this.headers,
|
||||
},
|
||||
timeout: this.timeout,
|
||||
})
|
||||
return response.status === 200
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(error.response)
|
||||
} else {
|
||||
logger.error(error)
|
||||
}
|
||||
return false
|
||||
}
|
||||
_createPage = async (page: NotionPage) => {
|
||||
await this._client.pages.create(page)
|
||||
}
|
||||
|
||||
export = async (items: LibraryItem[]): Promise<boolean> => {
|
||||
// find/create a parent page for all the items
|
||||
const parentPageName = 'Omnivore'
|
||||
|
||||
const pages = items.map(this._itemToNotionPage)
|
||||
await Promise.all(pages.map((page) => this._createPage(page)))
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,24 +5,27 @@ import { IntegrationClient } from './integration'
|
||||
|
||||
export class PocketClient implements IntegrationClient {
|
||||
name = 'POCKET'
|
||||
apiUrl = 'https://getpocket.com/v3'
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Accept': 'application/json',
|
||||
_token: string
|
||||
_axios = axios.create({
|
||||
baseURL: 'https://getpocket.com/v3',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Accept': 'application/json',
|
||||
},
|
||||
timeout: 5000, // 5 seconds
|
||||
})
|
||||
|
||||
constructor(token: string) {
|
||||
this._token = token
|
||||
}
|
||||
|
||||
accessToken = async (token: string): Promise<string | null> => {
|
||||
const url = `${this.apiUrl}/oauth/authorize`
|
||||
accessToken = async (): Promise<string | null> => {
|
||||
try {
|
||||
const response = await axios.post<{ access_token: string }>(
|
||||
url,
|
||||
const response = await this._axios.post<{ access_token: string }>(
|
||||
'/oauth/authorize',
|
||||
{
|
||||
consumer_key: env.pocket.consumerKey,
|
||||
code: token,
|
||||
},
|
||||
{
|
||||
headers: this.headers,
|
||||
timeout: 5000, // 5 seconds
|
||||
code: this._token,
|
||||
}
|
||||
)
|
||||
return response.data.access_token
|
||||
@ -45,15 +48,11 @@ export class PocketClient implements IntegrationClient {
|
||||
const redirectUri = `${env.client.url}/settings/integrations`
|
||||
|
||||
// make a POST request to Pocket to get a request token
|
||||
const response = await axios.post<{ code: string }>(
|
||||
`${this.apiUrl}/oauth/request`,
|
||||
const response = await this._axios.post<{ code: string }>(
|
||||
'/oauth/request',
|
||||
{
|
||||
consumer_key: consumerKey,
|
||||
redirect_uri: redirectUri,
|
||||
},
|
||||
{
|
||||
headers: this.headers,
|
||||
timeout: 5000, // 5 seconds
|
||||
}
|
||||
)
|
||||
const { code } = response.data
|
||||
|
||||
@ -33,17 +33,28 @@ interface ReadwiseHighlight {
|
||||
|
||||
export class ReadwiseClient implements IntegrationClient {
|
||||
name = 'READWISE'
|
||||
apiUrl = 'https://readwise.io/api/v2'
|
||||
_headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
_axios = axios.create({
|
||||
baseURL: 'https://readwise.io/api/v2',
|
||||
timeout: 5000, // 5 seconds
|
||||
})
|
||||
_token: string
|
||||
|
||||
accessToken = async (token: string): Promise<string | null> => {
|
||||
const authUrl = `${this.apiUrl}/auth`
|
||||
constructor(token: string) {
|
||||
this._token = token
|
||||
}
|
||||
|
||||
accessToken = async (): Promise<string | null> => {
|
||||
try {
|
||||
const response = await axios.get(authUrl, {
|
||||
const response = await this._axios.get('/auth', {
|
||||
headers: {
|
||||
Authorization: `Token ${token}`,
|
||||
...this._headers,
|
||||
Authorization: `Token ${this._token}`,
|
||||
},
|
||||
})
|
||||
return response.status === 204 ? token : null
|
||||
return response.status === 204 ? this._token : null
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(error.response)
|
||||
@ -54,14 +65,14 @@ export class ReadwiseClient implements IntegrationClient {
|
||||
}
|
||||
}
|
||||
|
||||
export = async (token: string, items: LibraryItem[]): Promise<boolean> => {
|
||||
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(token, highlights)
|
||||
result = await this._syncWithReadwise(highlights)
|
||||
}
|
||||
|
||||
return result
|
||||
@ -100,21 +111,18 @@ export class ReadwiseClient implements IntegrationClient {
|
||||
}
|
||||
|
||||
private _syncWithReadwise = async (
|
||||
token: string,
|
||||
highlights: ReadwiseHighlight[]
|
||||
): Promise<boolean> => {
|
||||
const url = `${this.apiUrl}/highlights`
|
||||
const response = await axios.post(
|
||||
url,
|
||||
const response = await this._axios.post(
|
||||
'/highlights',
|
||||
{
|
||||
highlights,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Token ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
...this._headers,
|
||||
Authorization: `Token ${this._token}`,
|
||||
},
|
||||
timeout: 5000, // 5 seconds
|
||||
}
|
||||
)
|
||||
return response.status === 200
|
||||
|
||||
24
yarn.lock
24
yarn.lock
@ -3931,6 +3931,14 @@
|
||||
"@nodelib/fs.scandir" "2.1.3"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@notionhq/client@^2.2.14":
|
||||
version "2.2.14"
|
||||
resolved "https://registry.yarnpkg.com/@notionhq/client/-/client-2.2.14.tgz#6807ec27ee89584529abfd28d058b2661f828b74"
|
||||
integrity sha512-oqUefZtCiJPCX+74A1Os9OVTef3fSnVWe2eVQtU1HJSD+nsfxfhwvDKnzJTh2Tw1ZHKLxpieHB/nzGdY+Uo12A==
|
||||
dependencies:
|
||||
"@types/node-fetch" "^2.5.10"
|
||||
node-fetch "^2.6.1"
|
||||
|
||||
"@npmcli/arborist@^5.6.3":
|
||||
version "5.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@npmcli/arborist/-/arborist-5.6.3.tgz#40810080272e097b4a7a4f56108f4a31638a9874"
|
||||
@ -7877,6 +7885,14 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node-fetch@^2.5.10", "@types/node-fetch@^2.6.4":
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24"
|
||||
integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^4.0.0"
|
||||
|
||||
"@types/node-fetch@^2.5.7":
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
|
||||
@ -7885,14 +7901,6 @@
|
||||
"@types/node" "*"
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node-fetch@^2.6.4":
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24"
|
||||
integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^4.0.0"
|
||||
|
||||
"@types/node-fetch@^2.6.6":
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.7.tgz#a1abe2ce24228b58ad97f99480fdcf9bbc6ab16d"
|
||||
|
||||
Reference in New Issue
Block a user