From a89fc38789c54ef8dfafc012f63abcf3893c850a Mon Sep 17 00:00:00 2001 From: myjeong19 Date: Tue, 25 Mar 2025 06:28:48 +0900 Subject: [PATCH 1/3] feat: Implement Twitter embed functionality --- package-lock.json | 254 ++++++++++++++++++++++++++++++++++++- package.json | 6 +- src/embed/tweet/index.ts | 1 + src/embed/tweet/process.ts | 26 ++++ src/embed/tweet/script.ts | 27 ++++ src/embed/tweet/tweet.ts | 199 +++++++++++++++++++++++++++++ src/embed/tweet/types.ts | 33 +++++ src/index.ts | 25 ++-- 8 files changed, 560 insertions(+), 11 deletions(-) create mode 100644 src/embed/tweet/index.ts create mode 100644 src/embed/tweet/process.ts create mode 100644 src/embed/tweet/script.ts create mode 100644 src/embed/tweet/tweet.ts create mode 100644 src/embed/tweet/types.ts diff --git a/package-lock.json b/package-lock.json index b2fdc11..35239bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.1", "license": "ISC", "dependencies": { - "@notionhq/client": "^2.2.3" + "@notionhq/client": "^2.2.3", + "cheerio": "^1.0.0" }, "devDependencies": { "cp": "^0.2.0", @@ -54,6 +55,51 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -73,6 +119,32 @@ "dev": true, "license": "MIT" }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -82,6 +154,80 @@ "node": ">=0.4.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/form-data": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", @@ -96,6 +242,35 @@ "node": ">= 6" } }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -137,6 +312,56 @@ } } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -157,6 +382,14 @@ "node": ">=4.2.0" } }, + "node_modules/undici": { + "version": "6.21.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", + "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -169,6 +402,25 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index 6b6904b..5b490e0 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "@notionpresso/api-sdk", "version": "0.0.1", "description": "", + "main": "./package/index.js", + "module": "./package/index.js", + "types": "./package/index.d.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "sync": "tsc --build .", @@ -23,7 +26,8 @@ "typescript": "^4.9.4" }, "dependencies": { - "@notionhq/client": "^2.2.3" + "@notionhq/client": "^2.2.3", + "cheerio": "^1.0.0" }, "type": "module", "exports": { diff --git a/src/embed/tweet/index.ts b/src/embed/tweet/index.ts new file mode 100644 index 0000000..007eb6c --- /dev/null +++ b/src/embed/tweet/index.ts @@ -0,0 +1 @@ +export * from './process.js'; diff --git a/src/embed/tweet/process.ts b/src/embed/tweet/process.ts new file mode 100644 index 0000000..e77ed1a --- /dev/null +++ b/src/embed/tweet/process.ts @@ -0,0 +1,26 @@ +import type { EmbedBlockObjectResponse } from '@notionhq/client/build/src/api-endpoints'; +import { convertToNotionpressoTweet } from './tweet'; + +/** + * 블록 배열에서 트위터/X 임베드를 notionpresso_tweet 형식으로 변환합니다. + * @param blocks 변환할 블록 배열 + * @returns 변환된 블록 배열 + */ +export function processTwitterEmbeds(blocks: any[]): any[] { + return blocks.map(block => { + if ('type' in block) { + if (block.type === 'embed') { + const embedBlock = block as EmbedBlockObjectResponse; + + if ( + embedBlock.embed.url && + (embedBlock.embed.url.includes('twitter') || embedBlock.embed.url.includes('x.com')) + ) { + console.log('트위터/X 임베드 변환:', embedBlock.embed.url); + return convertToNotionpressoTweet(embedBlock); + } + } + } + return block; + }); +} diff --git a/src/embed/tweet/script.ts b/src/embed/tweet/script.ts new file mode 100644 index 0000000..6705cf0 --- /dev/null +++ b/src/embed/tweet/script.ts @@ -0,0 +1,27 @@ +export const themeScript = ` + +`; diff --git a/src/embed/tweet/tweet.ts b/src/embed/tweet/tweet.ts new file mode 100644 index 0000000..e221a86 --- /dev/null +++ b/src/embed/tweet/tweet.ts @@ -0,0 +1,199 @@ +import type { OEmbedResponse } from './types.js'; +import { themeScript } from './script.js'; + +export async function fetchOEmbedData(url: string): Promise { + try { + const isTwitterUrl = isTwitter(url); + + if (isTwitterUrl) { + return await handleTwitterEmbed(url); + } + + return { + type: 'link', + provider_name: safeGetHostname(url), + title: 'View Link', + url: url, + }; + } catch (error) { + console.error('Failed to fetch OEmbed data:', error); + + return { + type: 'link', + provider_name: safeGetHostname(url), + title: 'View Link', + url: url, + }; + } +} + +function isTwitter(url: string): boolean { + try { + const hostname = new URL(url).hostname.toLowerCase(); + return hostname.includes('twitter.com') || hostname.includes('x.com'); + } catch (e) { + return false; + } +} + +export function convertToNotionpressoTweet(embedData: any): any { + if (embedData.type === 'embed') { + const embedUrl = embedData.embed?.url; + + if (embedUrl && (embedUrl.includes('twitter') || embedUrl.includes('x.com'))) { + const { embed, ...rest } = embedData; + + const tweetId = extractTweetId(embedUrl); + + if (!tweetId) { + console.error('Cannot extract tweet ID:', embedUrl); + return embedData; + } + + const iframeUrl = createTwitterEmbedUrl(tweetId, 'light'); + + const html = ` +
+ +
+ ${themeScript}`; + + return { + ...rest, + type: 'notionpresso_tweet', + notionpresso_tweet: { + url: embedUrl, + iframe_url: iframeUrl, + source: 'twitter', + tweet_id: tweetId, + html: html, + theme: 'auto', + }, + }; + } + } + + return embedData; +} + +async function handleTwitterEmbed(url: string): Promise { + try { + const tweetId = extractTweetId(url); + + if (!tweetId) { + throw new Error('Cannot extract tweet ID.'); + } + + const iframeUrl = createTwitterEmbedUrl(tweetId, 'light'); + + const html = ` +
+ +
+ ${themeScript}`; + + return { + type: 'notionpresso_tweet', + provider_name: 'Twitter', + provider_url: 'https://twitter.com', + url: url, + html: html, + width: 420, + height: 592, + title: 'Twitter Tweet', + notionpresso_tweet: { + url: url, + iframe_url: iframeUrl, + source: 'twitter', + tweet_id: tweetId, + theme: 'auto', + }, + } as OEmbedResponse; + } catch (error) { + console.error('Error processing Twitter embed:', error); + + return { + type: 'link', + provider_name: 'Twitter', + provider_url: 'https://twitter.com', + url: url, + title: 'View Tweet', + }; + } +} + +function extractTweetId(twitterUrl: string): string | null { + try { + const url = new URL(twitterUrl); + const pathname = url.pathname; + + const statusMatch = pathname.match(/\/status\/(\d+)/); + if (statusMatch && statusMatch[1]) { + return statusMatch[1]; + } + + const xMatch = pathname.match(/\/(\w+)\/status\/(\d+)/); + if (xMatch && xMatch[2]) { + return xMatch[2]; + } + + return null; + } catch (e) { + console.error('Error extracting tweet ID:', e); + return null; + } +} + +function createTwitterEmbedUrl(tweetId: string, theme: string = 'light'): string { + const baseUrl = 'https://platform.twitter.com/embed/Tweet.html'; + + const params = new URLSearchParams({ + id: tweetId, + dnt: 'true', + embedId: 'twitter-widget-0', + frame: 'false', + hideCard: 'false', + hideThread: 'false', + lang: 'en', + theme: theme, + siteScreenName: 'NotionpressoHQ', + widgetsVersion: '2b959255e8896:1673658205745', + width: '100%', + }); + + const features = + 'eyJ0ZndfdGltZWxpbmVfbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ'; + params.set('features', features); + + return `${baseUrl}?${params.toString()}`; +} + +function safeGetHostname(url: string): string { + try { + return new URL(url).hostname.replace(/^www\./, ''); + } catch (e) { + return 'link'; + } +} diff --git a/src/embed/tweet/types.ts b/src/embed/tweet/types.ts new file mode 100644 index 0000000..5654d97 --- /dev/null +++ b/src/embed/tweet/types.ts @@ -0,0 +1,33 @@ +export interface OEmbedResponse { + type: string; + version?: string; + title?: string; + url?: string; + provider_name: string; + provider_url?: string; + + width?: number; + height?: number; + html?: string; + + thumbnail_url?: string; + thumbnail_width?: number; + thumbnail_height?: number; + + author_name?: string; + author_url?: string; + + cache_age?: number; + description?: string; +} + +export interface NotionpressoTweetBlock { + type: 'notionpresso_tweet'; + notionpresso_tweet: { + url: string; + iframe_url: string; + source: string; + tweet_id?: string; + html: string; + }; +} diff --git a/src/index.ts b/src/index.ts index 0595fd3..b0d54ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,13 @@ -import {Client as _Client} from "@notionhq/client"; -import type {ClientOptions} from "@notionhq/client/build/src/Client"; +import { Client as _Client } from '@notionhq/client'; +import type { ClientOptions } from '@notionhq/client/build/src/Client'; import { BlockObjectResponse, PageObjectResponse, QueryDatabaseParameters, QueryDatabaseResponse, -} from "@notionhq/client/build/src/api-endpoints"; +} from '@notionhq/client/build/src/api-endpoints'; + +import { processTwitterEmbeds } from './embed/tweet'; export class Client extends _Client { constructor(options: ClientOptions = {}) { @@ -32,13 +34,13 @@ export class Client extends _Client { } while (cursor); } + blocks = processTwitterEmbeds(blocks); + const result = (await Promise.all( - (blocks as BlockObjectResponse[]).map(async (block) => { + (blocks as BlockObjectResponse[]).map(async block => { if (block.has_children) { - const blockId = - block.type === "synced_block" && - block.synced_block.synced_from != null + block.type === 'synced_block' && block.synced_block.synced_from != null ? block.synced_block.synced_from.block_id : block.id; @@ -65,8 +67,13 @@ export class Client extends _Client { async fetchPageListFromDatabase(params: QueryDatabaseParameters): Promise { const response = await this.databases.query(params); const result = [...response.results]; - if (response.has_more) { - const nextParams = {...params, database_id: response.next_cursor}; + if (response.has_more && response.next_cursor) { + const { database_id, ...restParams } = params; + const nextParams = { + database_id: database_id as string, + ...restParams, + start_cursor: response.next_cursor, + }; const nextResult = await this.fetchPageListFromDatabase(nextParams); result.push(...nextResult); } From 9e287d6d9c7784aa9c6ea0f678807692cff070f2 Mon Sep 17 00:00:00 2001 From: myjeong19 Date: Tue, 25 Mar 2025 06:32:42 +0900 Subject: [PATCH 2/3] refactor. delete JSdoc --- src/embed/tweet/process.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/embed/tweet/process.ts b/src/embed/tweet/process.ts index e77ed1a..88c44b4 100644 --- a/src/embed/tweet/process.ts +++ b/src/embed/tweet/process.ts @@ -1,11 +1,6 @@ import type { EmbedBlockObjectResponse } from '@notionhq/client/build/src/api-endpoints'; import { convertToNotionpressoTweet } from './tweet'; -/** - * 블록 배열에서 트위터/X 임베드를 notionpresso_tweet 형식으로 변환합니다. - * @param blocks 변환할 블록 배열 - * @returns 변환된 블록 배열 - */ export function processTwitterEmbeds(blocks: any[]): any[] { return blocks.map(block => { if ('type' in block) { From 9dec565c25b0caf53fe260d5de2fb58ce4b50edf Mon Sep 17 00:00:00 2001 From: myjeong19 Date: Wed, 26 Mar 2025 20:05:16 +0900 Subject: [PATCH 3/3] refactor. add Prettier --- .prettierrc | 6 +++ src/embed/tweet/index.ts | 2 +- src/embed/tweet/process.ts | 15 +++--- src/embed/tweet/tweet.ts | 98 +++++++++++++++++++++----------------- src/embed/tweet/types.ts | 2 +- src/index.ts | 19 ++++---- 6 files changed, 80 insertions(+), 62 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5a7704c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": false, + "trailingComma": "es5", + "tabWidth": 2, + "semi": true +} diff --git a/src/embed/tweet/index.ts b/src/embed/tweet/index.ts index 007eb6c..0bf818a 100644 --- a/src/embed/tweet/index.ts +++ b/src/embed/tweet/index.ts @@ -1 +1 @@ -export * from './process.js'; +export * from "./process.js"; diff --git a/src/embed/tweet/process.ts b/src/embed/tweet/process.ts index 88c44b4..604f6b4 100644 --- a/src/embed/tweet/process.ts +++ b/src/embed/tweet/process.ts @@ -1,17 +1,18 @@ -import type { EmbedBlockObjectResponse } from '@notionhq/client/build/src/api-endpoints'; -import { convertToNotionpressoTweet } from './tweet'; +import type { EmbedBlockObjectResponse } from "@notionhq/client/build/src/api-endpoints"; +import { convertToNotionpressoTweet } from "./tweet"; export function processTwitterEmbeds(blocks: any[]): any[] { - return blocks.map(block => { - if ('type' in block) { - if (block.type === 'embed') { + return blocks.map((block) => { + if ("type" in block) { + if (block.type === "embed") { const embedBlock = block as EmbedBlockObjectResponse; if ( embedBlock.embed.url && - (embedBlock.embed.url.includes('twitter') || embedBlock.embed.url.includes('x.com')) + (embedBlock.embed.url.includes("twitter") || + embedBlock.embed.url.includes("x.com")) ) { - console.log('트위터/X 임베드 변환:', embedBlock.embed.url); + console.log("트위터/X 임베드 변환:", embedBlock.embed.url); return convertToNotionpressoTweet(embedBlock); } } diff --git a/src/embed/tweet/tweet.ts b/src/embed/tweet/tweet.ts index e221a86..16defc6 100644 --- a/src/embed/tweet/tweet.ts +++ b/src/embed/tweet/tweet.ts @@ -1,7 +1,9 @@ -import type { OEmbedResponse } from './types.js'; -import { themeScript } from './script.js'; +import type { OEmbedResponse } from "./types.js"; +import { themeScript } from "./script.js"; -export async function fetchOEmbedData(url: string): Promise { +export async function fetchOEmbedData( + url: string +): Promise { try { const isTwitterUrl = isTwitter(url); @@ -10,18 +12,18 @@ export async function fetchOEmbedData(url: string): Promise @@ -71,14 +76,14 @@ export function convertToNotionpressoTweet(embedData: any): any { return { ...rest, - type: 'notionpresso_tweet', + type: "notionpresso_tweet", notionpresso_tweet: { url: embedUrl, iframe_url: iframeUrl, - source: 'twitter', + source: "twitter", tweet_id: tweetId, html: html, - theme: 'auto', + theme: "auto", }, }; } @@ -92,10 +97,10 @@ async function handleTwitterEmbed(url: string): Promise { const tweetId = extractTweetId(url); if (!tweetId) { - throw new Error('Cannot extract tweet ID.'); + throw new Error("Cannot extract tweet ID."); } - const iframeUrl = createTwitterEmbedUrl(tweetId, 'light'); + const iframeUrl = createTwitterEmbedUrl(tweetId, "light"); const html = `
@@ -115,31 +120,31 @@ async function handleTwitterEmbed(url: string): Promise { ${themeScript}`; return { - type: 'notionpresso_tweet', - provider_name: 'Twitter', - provider_url: 'https://twitter.com', + type: "notionpresso_tweet", + provider_name: "Twitter", + provider_url: "https://twitter.com", url: url, html: html, width: 420, height: 592, - title: 'Twitter Tweet', + title: "Twitter Tweet", notionpresso_tweet: { url: url, iframe_url: iframeUrl, - source: 'twitter', + source: "twitter", tweet_id: tweetId, - theme: 'auto', + theme: "auto", }, } as OEmbedResponse; } catch (error) { - console.error('Error processing Twitter embed:', error); + console.error("Error processing Twitter embed:", error); return { - type: 'link', - provider_name: 'Twitter', - provider_url: 'https://twitter.com', + type: "link", + provider_name: "Twitter", + provider_url: "https://twitter.com", url: url, - title: 'View Tweet', + title: "View Tweet", }; } } @@ -161,39 +166,42 @@ function extractTweetId(twitterUrl: string): string | null { return null; } catch (e) { - console.error('Error extracting tweet ID:', e); + console.error("Error extracting tweet ID:", e); return null; } } -function createTwitterEmbedUrl(tweetId: string, theme: string = 'light'): string { - const baseUrl = 'https://platform.twitter.com/embed/Tweet.html'; +function createTwitterEmbedUrl( + tweetId: string, + theme: string = "light" +): string { + const baseUrl = "https://platform.twitter.com/embed/Tweet.html"; const params = new URLSearchParams({ id: tweetId, - dnt: 'true', - embedId: 'twitter-widget-0', - frame: 'false', - hideCard: 'false', - hideThread: 'false', - lang: 'en', + dnt: "true", + embedId: "twitter-widget-0", + frame: "false", + hideCard: "false", + hideThread: "false", + lang: "en", theme: theme, - siteScreenName: 'NotionpressoHQ', - widgetsVersion: '2b959255e8896:1673658205745', - width: '100%', + siteScreenName: "NotionpressoHQ", + widgetsVersion: "2b959255e8896:1673658205745", + width: "100%", }); const features = - 'eyJ0ZndfdGltZWxpbmVfbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ'; - params.set('features', features); + "eyJ0ZndfdGltZWxpbmVfbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ"; + params.set("features", features); return `${baseUrl}?${params.toString()}`; } function safeGetHostname(url: string): string { try { - return new URL(url).hostname.replace(/^www\./, ''); + return new URL(url).hostname.replace(/^www\./, ""); } catch (e) { - return 'link'; + return "link"; } } diff --git a/src/embed/tweet/types.ts b/src/embed/tweet/types.ts index 5654d97..2c6e490 100644 --- a/src/embed/tweet/types.ts +++ b/src/embed/tweet/types.ts @@ -22,7 +22,7 @@ export interface OEmbedResponse { } export interface NotionpressoTweetBlock { - type: 'notionpresso_tweet'; + type: "notionpresso_tweet"; notionpresso_tweet: { url: string; iframe_url: string; diff --git a/src/index.ts b/src/index.ts index b0d54ff..afb48af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,13 @@ -import { Client as _Client } from '@notionhq/client'; -import type { ClientOptions } from '@notionhq/client/build/src/Client'; +import { Client as _Client } from "@notionhq/client"; +import type { ClientOptions } from "@notionhq/client/build/src/Client"; import { BlockObjectResponse, PageObjectResponse, QueryDatabaseParameters, QueryDatabaseResponse, -} from '@notionhq/client/build/src/api-endpoints'; +} from "@notionhq/client/build/src/api-endpoints"; -import { processTwitterEmbeds } from './embed/tweet'; +import { processTwitterEmbeds } from "./embed/tweet"; export class Client extends _Client { constructor(options: ClientOptions = {}) { @@ -37,10 +37,11 @@ export class Client extends _Client { blocks = processTwitterEmbeds(blocks); const result = (await Promise.all( - (blocks as BlockObjectResponse[]).map(async block => { + (blocks as BlockObjectResponse[]).map(async (block) => { if (block.has_children) { const blockId = - block.type === 'synced_block' && block.synced_block.synced_from != null + block.type === "synced_block" && + block.synced_block.synced_from != null ? block.synced_block.synced_from.block_id : block.id; @@ -64,7 +65,9 @@ export class Client extends _Client { return { ...page, blocks }; } - async fetchPageListFromDatabase(params: QueryDatabaseParameters): Promise { + async fetchPageListFromDatabase( + params: QueryDatabaseParameters + ): Promise { const response = await this.databases.query(params); const result = [...response.results]; if (response.has_more && response.next_cursor) { @@ -84,6 +87,6 @@ export class Client extends _Client { export type Block = BlockObjectResponse & { blocks: Block[] }; export type ContentfulPage = PageObjectResponse & { blocks: Block[] }; -export type QueryDatabaseResults = QueryDatabaseResponse['results']; +export type QueryDatabaseResults = QueryDatabaseResponse["results"]; export { ClientOptions }; export default Client;