From 6b9290d7b5192eec19e5542dc0775c20783a07ab Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Thu, 19 Sep 2024 12:06:27 +0100 Subject: [PATCH] Pause for code --- src/commands/NotionCommands.ts | 1 + src/upload/Upload2Notion.ts | 526 ++++++++++++++++++ .../{BaseUpload2Notion.ts => UploadBase.ts} | 8 + src/upload/getMarkdown.ts | 140 +++++ 4 files changed, 675 insertions(+) create mode 100644 src/upload/Upload2Notion.ts rename src/upload/{BaseUpload2Notion.ts => UploadBase.ts} (78%) create mode 100644 src/upload/getMarkdown.ts diff --git a/src/commands/NotionCommands.ts b/src/commands/NotionCommands.ts index 0953dd0..f5f7dbf 100644 --- a/src/commands/NotionCommands.ts +++ b/src/commands/NotionCommands.ts @@ -85,6 +85,7 @@ export default class RibbonCommands { let commandName = `Share to ${dbDetails.fullName} (${dbDetails.abName})`; // or use a translated name let editorCallback: (editor: Editor, view: MarkdownView) => Promise; + if (dbDetails.format === 'next') { editorCallback = async (editor, view) => { await uploadCommandNext(this.plugin, this.plugin.settings, dbDetails, this.plugin.app); diff --git a/src/upload/Upload2Notion.ts b/src/upload/Upload2Notion.ts new file mode 100644 index 0000000..c475566 --- /dev/null +++ b/src/upload/Upload2Notion.ts @@ -0,0 +1,526 @@ +import {App, Notice, Platform, TFile, requestUrl} from "obsidian"; +import {markdownToBlocks} from "@jxpeng98/martian"; +import * as yamlFrontMatter from "yaml-front-matter"; +import MyPlugin from "src/main"; +import {DatabaseDetails, PluginSettings} from "../../ui/settingTabs"; +import {updateYamlInfo} from "../updateYaml"; +import {UploadBase} from "./UploadBase"; +import fetch from 'node-fetch'; +import {i18nConfig} from "../../lang/I18n"; + +interface CreatePageResponse { + response: any; + data: any; +} + +export class Upload2Notion extends UploadBase { + settings: PluginSettings; + dbDetails: DatabaseDetails; + + constructor(plugin: MyPlugin, dbDetails: DatabaseDetails) { + super(plugin, dbDetails); + this.dbDetails = dbDetails; + } + + async updatePage( + notionID: string, + cover: string, + Values: Record, + childArr: any, + ) { + await this.deletePage(notionID); + + const {databaseID} = this.dbDetails; + + const databaseCover = await this.getDataBase(databaseID); + + if (cover == null) { + cover = databaseCover; + } + + return await this.createPage(cover, Values, childArr); + } + + async createPage( + cover: string, + Values: Record, + childArr: any, + ): Promise { + + // general + const { + format, + databaseID, + notionAPI + } = this.dbDetails; + + // next + const { + format, + databaseID, + notionAPI + } = this.dbDetails; + + // custom + const { + format, + databaseID, + customProperties, + notionAPI + } = this.dbDetails; + + // remove the annotations from the childArr if type is code block + childArr.forEach((block: any) => { + if (block.type === "code") { + block.code.rich_text.forEach((item: any) => { + if (item.type === "text" && item.annotations) { + delete item.annotations; + } + } + ); + } + } + ); + + // check the length of the childArr and split it into chunks of 100 + const childArrLength = childArr.length; + let extraArr: any[] = []; + let firstArr: any; + let pushCount = 0; + + console.log(`Page includes ${childArrLength} blocks`) + + if (childArrLength > 100) { + for (let i = 0; i < childArr.length; i += 100) { + if (i == 0) { + firstArr = childArr.slice(0, 100); + } else { + const chunk = childArr.slice(i, i + 100); + extraArr.push(chunk); + pushCount++; + } + } + } else { + firstArr = childArr; + } + + // modify for all type of format + const bodyString: any = this.buildBodyString(customProperties, Values, firstArr); + + // general + if (format === "general") { + const bodyString: any = { + parent: { + database_id: databaseID, + }, + properties: { + [customTitleButton + ? customTitleName + : "title"]: { + title: [ + { + text: { + content: title, + }, + }, + ], + }, + ...(tagButton + ? { + tags: { + multi_select: tags && true ? tags.map((tag) => ({name: tag})) : [], + }, + } + : {}), + }, + children: firstArr, + }; + } + + // next + if (format === "next") { + // parse the values to the bodyString + } + + + // apply for all type of format + if (cover) { + bodyString.cover = { + type: "external", + external: { + url: cover, + }, + }; + } + + if (!bodyString.cover && this.plugin.settings.bannerUrl) { + bodyString.cover = { + type: "external", + external: { + url: this.plugin.settings.bannerUrl, + }, + }; + } + + console.log(bodyString) + + console.log(Platform.isDesktopApp) + + let response: any; + let data: any; + + if (Platform.isMobileApp) { + if(childArrLength > 100) { + new Notice(i18nConfig["reach-mobile-limit"], 5000); + } else { + const {response} = await this.fetchMobile(bodyString); + } + } + + if (Platform.isDesktopApp) { + + const {response, data} = await this.fetchDesktopFirst(bodyString); + + // upload the rest of the blocks + if (pushCount > 0) { + for (let i = 0; i < pushCount; i++) { + const extraBlocks = {children: extraArr[i]}; + + console.log(extraBlocks) + + const {extraResponse, extraData} = await this.fetchDesktopExtra(extraBlocks, data); + + } + } + } + + return { + response, // for status code + data // for id and url + } + } + + async fetchMobile(bodyString: any) { + return await requestUrl({ + url: `https://api.notion.com/v1/pages`, + method: "POST", + headers: { + "Content-Type": "application/json", + // 'User-Agent': 'obsidian.md', + Authorization: + "Bearer " + this.dbDetails.notionAPI, + "Notion-Version": "2022-06-28", + }, + body: JSON.stringify(bodyString), + }); + } + + async fetchDesktopFirst(bodyString: any) { + response = await fetch("https://api.notion.com/v1/pages", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + notionAPI, + "Notion-Version": "2022-06-28", + }, + body: JSON.stringify(bodyString), + }); + + data = await response.json(); + + if (!response.ok) { + new Notice(`Error ${data.status}: ${data.code} \n ${i18nConfig["CheckConsole"]}`, 5000); + console.log(`Error message: \n ${data.message}`); + } else { + console.log(`Page created: ${data.url}`); + console.log(`Page ID: ${data.id}`); + } + + return {response, data}; + } + + async fetchDesktopExtra(extraBlocks: any, data: any) { + const extraResponse = await fetch(`https://api.notion.com/v1/blocks/${data.id}/children`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + notionAPI, + "Notion-Version": "2022-06-28", + }, + body: JSON.stringify(extraBlocks), + }); + + const extraData: any = await extraResponse.json(); + + if (!extraResponse.ok) { + new Notice(`Error ${extraData.status}: ${extraData.code} \n ${i18nConfig["CheckConsole"]}`, 5000); + console.log(`Error message: \n ${extraData.message}`); + } else { + console.log(`${i18nConfig["ExtraBlockUploaded"]} to page: ${data.id}`); + if (i === pushCount - 1) { + console.log(`${i18nConfig["BlockUploaded"]} to page: ${data.id}`); + new Notice(`${i18nConfig["BlockUploaded"]} page: ${data.id}`, 5000); + } + } + + return {extraResponse, extraData}; + } + + async syncMarkdownToNotion( + cover: string, + Values: Record, + markdown: string, + nowFile: TFile, + app: App, + ): Promise { + const options = { + strictImageUrls: true, + notionLimits: { + truncate: false, + } + } + let res: any; + const yamlContent: any = yamlFrontMatter.loadFront(markdown); + const __content = yamlContent.__content; + const file2Block = markdownToBlocks(__content, options); + const frontMatter = app.metadataCache.getFileCache(nowFile)?.frontmatter; + const {abName} = this.dbDetails + const notionIDKey = `NotionID-${abName}`; + const notionID = frontMatter ? frontMatter[notionIDKey] : null; + + if (notionID) { + res = await this.updatePage( + notionID, + cover, + Values, + file2Block, + ); + } else { + res = await this.createPage(cover, Values, file2Block); + } + + let {response, data} = res; + + // console.log(response) + + if (Platform.isDesktopApp) { + if (response && response.status === 200) { + await updateYamlInfo( + markdown, + nowFile, + data, + app, + this.plugin, + this.dbDetails + ); + } + } + + if (Platform.isMobileApp) { + if (response && response.status === 200) { + await updateYamlInfo( + markdown, + nowFile, + response, + app, + this.plugin, + this.dbDetails, + ); + } + } + + return res; + } + + private buildPropertyObjectCustom(customName: string, customType: string, Values: Record) { + const value = Values[customName] || ''; + + switch (customType) { + case "title": + return { + title: [ + { + text: { + content: value, + }, + }, + ], + }; + case "rich_text": + return { + rich_text: [ + { + text: { + content: value || '', + }, + }, + ], + }; + case "date": + return { + date: { + start: value || new Date().toISOString(), + }, + }; + case "number": + return { + number: Number(value), + }; + case "phone_number": + return { + phone_number: value, + }; + case "email": + return { + email: value, + }; + case "url": + return { + url: value, + }; + case "files": + return { + files: Array.isArray(value) ? value.map(url => ({ + name: url, + type: "external", + external: { + url: url, + }, + })) : [ + { + name: value, + type: "external", + external: { + url: value, + }, + }, + ], + }; + case "checkbox": + return { + checkbox: Boolean(value) || false, + }; + case "select": + return { + select: { + name: value, + }, + }; + case "multi_select": + return { + multi_select: Array.isArray(value) ? value.map(item => ({name: item})) : [{name: value}], + }; + // add cases for next + case "type": + return { + select: { + name: value || 'Post', + }, + }; + case "status": + return { + select: { + name: value || 'Draft', + }, + }; + case "category": + return { + select: { + name: value || 'Obsidian', + }, + }; + case "password": + return { + rich_text: [ + { + text: { + content: value || '', + }, + }, + ], + }; + case "icon": + return { + rich_text: [ + { + text: { + content: value || '', + }, + }, + ], + }; + case "date": + return { + date: { + start: value || new Date().toISOString(), + }, + }; + case "slug": + return { + rich_text: [ + { + text: { + content: value || '', + }, + }, + ], + }; + case "summary": + return { + rich_text: [ + { + text: { + content: value || '', + }, + }, + ], + }; + // tags can be used for next and general + case "tags": + return { + multi_select: Array.isArray(value) ? value.map(item => ({name: item})) : [{name: value}], + }; + } + } + + private buildBodyStringCustom( + customProperties: { customName: string; customType: string }[], + customValues: Record, + childArr: any, + ) { + + const properties: { [key: string]: any } = {}; + + // Only include custom properties that have values + customProperties.forEach(({customName, customType}) => { + if (customValues[customName] !== undefined) { + properties[customName] = this.buildPropertyObjectCustom(customName, customType, customValues); + } + } + ); + + // console.log(properties) + + return { + parent: { + database_id: this.dbDetails.databaseID, + }, + properties, + children: childArr, + }; + } + + private buildBodyStringGeneral( + Values: Record, + childArr: any, + ) { + + } + + private buildBodyStringNext( + Values: Record, + childArr: any, + ) { + + } +} diff --git a/src/upload/BaseUpload2Notion.ts b/src/upload/UploadBase.ts similarity index 78% rename from src/upload/BaseUpload2Notion.ts rename to src/upload/UploadBase.ts index 9b4a277..70d17d3 100644 --- a/src/upload/BaseUpload2Notion.ts +++ b/src/upload/UploadBase.ts @@ -1,3 +1,11 @@ +import { App, Notice, requestUrl, TFile } from "obsidian"; +import { Client } from '@notionhq/client'; +import { markdownToBlocks } from "@jxpeng98/martian"; +import * as yamlFrontMatter from "yaml-front-matter"; +import MyPlugin from "src/main"; +import { DatabaseDetails } from "../../ui/settingTabs"; + + export class UploadBase { plugin: MyPlugin; notion: Client; diff --git a/src/upload/getMarkdown.ts b/src/upload/getMarkdown.ts new file mode 100644 index 0000000..bd65f90 --- /dev/null +++ b/src/upload/getMarkdown.ts @@ -0,0 +1,140 @@ +import { App, Notice } from "obsidian"; +import { i18nConfig } from "../../lang/I18n"; +import { DatabaseDetails } from "../../ui/settingTabs"; + +interface MarkdownData { + markDownData: string; + nowFile: TFile; + cover: string; + [key: string]: any; +} + +interface GeneralValues { + title: string; + tags: string[]; +} + +interface NextValues { + title: string; + emoji: string; + tags: string[]; + type: string; + slug: string; + stats: string; + category: string; + summary: string; + paword: string; + favicon: string; + datetime: string; +} + +export async function getMarkdown( + app: App, + dbDetails: DatabaseDetails, +): Promise { + const nowFile = app.workspace.getActiveFile(); + + if (!nowFile) { + new Notice(i18nConfig["open-file"]); + return; + } + + const markDownData = await nowFile.vault.read(nowFile); + + const { format } = dbDetails; // general, next or custom + const cover = fileCache.frontmatter.coverurl || ''; + const FileCache = app.metadataCache.getFileCache(nowFile); + let result: MarkdownData = { markDownData, cover }; // title will be saved in the values object + + try { + switch (format) { + case "general": + result = { ...result, ...getGeneralFormat(FileCache, dbDetails, nowFile) }; + break; + case "next": + result = { ...result, ...getNextFormat(FileCache, nowFile) }; + break; + case "custom": + result = { ...result, ...getCustomFormat(FileCache, dbDetails, nowFile) }; + break; + default: + throw new Error(`Unsupported format: ${format}`); + } + } catch (error) { + console.error('Error processing markdown:', error); + new Notice(i18nConfig["set-tags-fail"]); + } + + return result; +} + +// TODO: use the front matter as the title + +function getGeneralFormat(fileCache: FileCache, nowFile: TFile): Record { + const { customTitleButton, customTitleName, tagButton } = dbDetails; + const GeneralValues: GeneralValues = { + title: '', + tags: [], + }; + + if (fileCache && fileCache.frontmatter) { + GeneralValues.title = fileCache.frontmatter.title || nowFile.basename; + GeneralValues.tags = fileCache.frontmatter.tags || []; + } + + return { GeneralValues }; +} + +function getNextFormat(fileCache: FileCache, nowFile: TFile): Record { + const NextValues: NextValues = { + title: '', + emoji: '', + tags: [], + type: '', + slug: '', + stats: '', + category: '', + summary: '', + paword: '', + favicon: '', + datetime: '' + }; + + if (fileCache && fileCache.frontmatter) { + NextValues.title = fileCache.frontmatter.title || nowFile.basename; + NextValues.emoji = fileCache.frontmatter.titleicon || ''; + NextValues.tags = fileCache.frontmatter.tags || []; + NextValues.type = fileCache.frontmatter.type || ''; + NextValues.slug = fileCache.frontmatter.slug || ''; + NextValues.stats = fileCache.frontmatter.stats || fileCache.frontmatter.status || ''; + NextValues.category = fileCache.frontmatter.category || ''; + NextValues.summary = fileCache.frontmatter.summary || ''; + NextValues.paword = fileCache.frontmatter.password || ''; + NextValues.favicon = fileCache.frontmatter.icon || ''; + NextValues.datetime = fileCache.frontmatter.date || ''; + } + + return { NextValues }; +} + +function getCustomFormat(fileCache: FileCache, dbDetails: DatabaseDetails, nowFile: TFile): Record { + const customValues: Record = {}; + + dbDetails.customProperties + .filter(property => property.customType !== 'title') // Exclude 'title' type property + .forEach(({ customName }) => { + if (fileCache.frontmatter[customName] !== undefined) { + customValues[customName] = fileCache.frontmatter[customName]; + } + }); + + const titleProperty = dbDetails.customProperties.find(property => property.customType === 'title'); + + // If a 'title' type property exists, use the file's basename as its value + + if (titleProperty) { + customValues[titleProperty.customName] = nowFile.basename; // Use 'basename' for the file name without extension + } + + return { customValues }; +}