Merge pull request #3674 from omnivore-app/fix/notion

fix/notion
This commit is contained in:
Hongbo Wu 2024-03-15 08:16:07 +08:00 committed by GitHub
commit ced92fb1db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 150 additions and 74 deletions

View file

@ -20,7 +20,7 @@ export interface RetrieveRequest {
export interface IntegrationClient {
name: string
_token: string
token: string
accessToken(): Promise<string | null>

View file

@ -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'
@ -61,10 +62,10 @@ interface NotionPage {
}>
}
'Original URL': {
url: string | null
url: string
}
'Omnivore URL': {
url: string | null
url: string
}
Tags?: {
multi_select: Array<{ name: string }>
@ -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<Integration, { settings?: Settings }>
private client: Client
private integrationData?: Merge<Integration, { settings?: Settings }>
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<string | null> => {
@ -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}`,
},
}
@ -158,15 +163,13 @@ export class NotionClient implements IntegrationClient {
return Promise.resolve(env.notion.authUrl)
}
private _itemToNotionPage = (item: LibraryItem): NotionPage => {
const databaseId = this._integrationData?.settings?.parentDatabaseId
if (!databaseId) {
throw new Error('Notion database id not found')
}
private itemToNotionPage = (
item: LibraryItem,
settings: Settings
): NotionPage => {
return {
parent: {
database_id: databaseId,
database_id: settings.parentDatabaseId,
},
icon: item.siteIcon
? {
@ -207,9 +210,14 @@ export class NotionClient implements IntegrationClient {
'Omnivore URL': {
url: `${env.client.url}/me/${item.slug}`,
},
Tags: item.labels
? { multi_select: item.labels.map((label) => ({ name: label.name })) }
: undefined,
Tags:
item.labels && settings.properties.includes('labels')
? {
multi_select: item.labels.map((label) => ({
name: label.name,
})),
}
: undefined,
},
children: item.highlights
? item.highlights.map((highlight) => ({
@ -218,7 +226,12 @@ export class NotionClient implements IntegrationClient {
rich_text: [
{
text: {
content: highlight.quote || '',
content: settings.properties.includes('highlights')
? highlight.quote || ''
: '',
link: {
url: highlightUrl(item.slug, highlight.id),
},
},
annotations: {
color: highlight.color as AnnotationColor,
@ -226,7 +239,9 @@ export class NotionClient implements IntegrationClient {
},
{
text: {
content: `\n${highlight.annotation || ''}`,
content: settings.properties.includes('notes')
? `\n${highlight.annotation || ''}`
: '',
},
annotations: {
italic: true,
@ -239,26 +254,45 @@ export class NotionClient implements IntegrationClient {
}
}
_createPage = async (page: NotionPage) => {
await this._client.pages.create(page)
private createPage = async (page: NotionPage) => {
await this.client.pages.create(page)
}
private findPage = async (url: string, databaseId: string) => {
const response = await this.client.databases.query({
database_id: databaseId,
page_size: 1,
filter: {
property: 'Omnivore URL',
url: {
equals: url,
},
},
})
if (response.results.length > 0) {
return response.results[0]
}
return null
}
export = async (items: LibraryItem[]): Promise<boolean> => {
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
}
const 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,
},
@ -296,18 +330,67 @@ export class NotionClient implements IntegrationClient {
})
// save the database id
this._integrationData.settings.parentDatabaseId = database.id
databaseId = database.id
await updateIntegration(
this._integrationData.id,
this.integrationData.id,
{
settings: this._integrationData.settings,
settings: {
...this.integrationData.settings,
parentDatabaseId: databaseId,
},
},
this._integrationData.user.id
this.integrationData.user.id
)
}
const pages = items.map(this._itemToNotionPage)
await Promise.all(pages.map((page) => this._createPage(page)))
await Promise.all(
items.map(async (item) => {
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({
page_id: existingPage.id,
properties: notionPage.properties,
})
const children = notionPage.children
if (children) {
// get the existing children
const response = await this.client.blocks.children.list({
block_id: existingPage.id,
})
if (response.results.length > 0) {
const existingChildren =
response.results as NotionPage['children']
// delete the existing children from children
notionPage.children = children.filter(
(child) =>
!existingChildren?.some(
(existingChild) =>
existingChild.paragraph.rich_text[0].text.link?.url ===
child.paragraph.rich_text[0].text.link?.url
)
)
}
// append the children
if (notionPage.children && notionPage.children.length > 0) {
await this.client.blocks.children.append({
block_id: existingPage.id,
children: notionPage.children,
})
}
}
return
}
// create the page
return this.createPage(notionPage)
})
)
return true
}

View file

@ -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<string | null> => {
@ -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

View file

@ -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<string | null> => {
@ -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}`,
},
}
)

View file

@ -176,7 +176,7 @@ export default function Integrations(): JSX.Element {
token,
name: 'NOTION',
type: 'EXPORT',
enabled: true,
enabled: false,
})
showSuccessToast('Connected with Notion.')

View file

@ -12,7 +12,7 @@ import 'antd/dist/antd.compact.css'
import { CheckboxValueType } from 'antd/lib/checkbox/Group'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useEffect, useMemo } from 'react'
import { HStack, VStack } from '../../../components/elements/LayoutPrimitives'
import { PageMetaData } from '../../../components/patterns/PageMetaData'
import { Beta } from '../../../components/templates/Beta'
@ -20,17 +20,14 @@ import { Header } from '../../../components/templates/settings/SettingsTable'
import { SettingsLayout } from '../../../components/templates/SettingsLayout'
import { deleteIntegrationMutation } from '../../../lib/networking/mutations/deleteIntegrationMutation'
import { setIntegrationMutation } from '../../../lib/networking/mutations/setIntegrationMutation'
import {
Integration,
useGetIntegrationsQuery,
} from '../../../lib/networking/queries/useGetIntegrationsQuery'
import { useGetIntegrationsQuery } from '../../../lib/networking/queries/useGetIntegrationsQuery'
import { applyStoredTheme } from '../../../lib/themeUpdater'
import { showSuccessToast } from '../../../lib/toastHelpers'
type FieldType = {
parentPageId?: string
parentDatabaseId?: string
autoSync?: boolean
enabled: boolean
properties?: string[]
}
@ -39,27 +36,21 @@ export default function Notion(): JSX.Element {
const router = useRouter()
const { integrations, revalidate } = useGetIntegrationsQuery()
const [notion, setNotion] = useState<Integration>()
const notion = useMemo(() => {
return integrations.find((i) => i.name == 'NOTION' && i.type == 'EXPORT')
}, [integrations])
const [form] = Form.useForm<FieldType>()
const [messageApi, contextHolder] = message.useMessage()
useEffect(() => {
const notion = integrations.find(
(i) => i.name == 'NOTION' && i.type == 'EXPORT'
)
if (notion) {
setNotion(notion)
form.setFieldsValue({
parentPageId: notion.settings?.parentPageId,
parentDatabaseId: notion.settings?.parentDatabaseId,
autoSync: notion.settings?.autoSync,
properties: notion.settings?.properties,
})
}
}, [form, integrations])
form.setFieldsValue({
parentPageId: notion?.settings?.parentPageId,
parentDatabaseId: notion?.settings?.parentDatabaseId,
enabled: notion?.enabled,
properties: notion?.settings?.properties,
})
}, [form, notion])
const deleteNotion = async () => {
if (!notion) {
@ -69,6 +60,7 @@ export default function Notion(): JSX.Element {
await deleteIntegrationMutation(notion.id)
showSuccessToast('Notion integration disconnected successfully.')
revalidate()
router.push('/settings/integrations')
}
@ -82,7 +74,7 @@ export default function Notion(): JSX.Element {
name: notion.name,
type: notion.type,
token: notion.token,
enabled: notion.enabled,
enabled: values.enabled,
settings: values,
})
}
@ -170,7 +162,7 @@ export default function Notion(): JSX.Element {
<Form.Item<FieldType>
label="Automatic Sync"
name="autoSync"
name="enabled"
valuePropName="checked"
>
<Switch />