Tom Butcher ccfa654016
All checks were successful
thehideout/TheHideout-API/pipeline/head This commit looks good
Minor fixes.
2026-01-03 00:32:21 +00:00

574 lines
17 KiB
JavaScript

import { processNotionIcon } from "./icon";
import { env } from "cloudflare:workers";
import { getNavigation } from "./navigation";
const NOTION_API_BASE = "https://api.notion.com/v1";
const NOTION_VERSION = "2025-09-03"; // or your preferred version
const iconColorSubstitutions = [
["#D44C47", "#FF453A"],
["#55534E", "#FFFFFF"],
["#448361", "#32D74B"],
["#337ea9", "#0A84FF"],
["#9065B0", "#BF5AF2"],
["#CB912F", "#FFD60A"],
["#C14C8A", "#FF375F"],
["#d9730d", "#FF9F0A"],
];
async function notionFetch(endpoint, options = {}) {
const res = await fetch(`${NOTION_API_BASE}${endpoint}`, {
headers: {
Authorization: `Bearer ${env.NOTION_AUTH}`,
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
},
...options,
});
if (!res.ok) {
const text = await res.text();
throw new Error(
`Notion API error: ${res.status} ${res.statusText} - ${text}`
);
}
return res.json();
}
export async function addToNotionDataSource(
properties,
dataSourceId,
icon = null
) {
console.log("Adding to Notion data source...");
console.log("Properties:", properties);
const pageParams = {};
const notionProperties = {};
if (icon != null) {
pageParams["icon"] = {
type: "external",
external: {
url: icon,
},
};
}
for (const [key, value] of Object.entries(properties)) {
if (key === "Email" && value != "") {
notionProperties[key] = { email: value };
} else if (key === "Name" && value != "") {
notionProperties[key] = { title: [{ text: { content: value } }] };
} else if (key === "Features") {
notionProperties[key] = {
multi_select: value.map((feature) => ({
name: feature,
})),
};
} else if (key === "Message" && value != "") {
pageParams["children"] = [
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [
{
type: "text",
text: {
content: value,
},
},
],
},
},
];
} else if (typeof value === "string" && value != "") {
notionProperties[key] = {
rich_text: [{ text: { content: value } }],
};
} else if (typeof value === "number" && value != undefined) {
notionProperties[key] = { number: value };
} else if (value && typeof value === "object" && !Array.isArray(value)) {
notionProperties[key] = value;
}
}
try {
const response = await notionFetch("/pages", {
method: "POST",
body: JSON.stringify({
parent: { data_source_id: dataSourceId },
...pageParams,
properties: notionProperties,
}),
});
console.log("Added to Notion DB!");
return response;
} catch (error) {
console.error("Failed to add to Notion:", error);
throw new Error("Failed to add to Notion");
}
}
export async function queryNotionDataSource(dataSourceId, queryParams = {}) {
console.log(`Fetching data source: ${dataSourceId} from Notion API...`);
try {
const body = JSON.stringify(queryParams);
const result = await notionFetch(`/data_sources/${dataSourceId}/query`, {
method: "POST",
body,
});
console.log("Fetched data source:", dataSourceId);
return result.results;
} catch (error) {
console.error(`Error fetching Notion data source ${dataSourceId}:`, error);
throw error;
}
}
export async function getNotionDatabase(databaseId) {
console.log(`Fetching database: ${databaseId} from Notion API...`);
try {
const result = await notionFetch(`/databases/${databaseId}`, {
method: "GET",
});
return result;
} catch (error) {
console.error(`Error fetching Notion database ${databaseId}:`, error);
throw error;
}
}
export async function getNotionDataSource(dataSourceId) {
console.log(`Fetching data source: ${dataSourceId} from Notion API...`);
try {
const result = await notionFetch(`/data_sources/${dataSourceId}`, {
method: "GET",
});
return result;
} catch (error) {
console.error(`Error fetching Notion data source ${dataSourceId}:`, error);
throw error;
}
}
export async function getNotionPage(pageId) {
console.log(`Fetching page: ${pageId} from Notion API...`);
try {
const result = await notionFetch(`/pages/${pageId}`, {
method: "GET",
});
return result;
} catch (error) {
console.error(`Error fetching Notion page ${pageId}:`, error);
throw error;
}
}
export async function updateNotionPage(
pageId,
properties,
archive = false,
trash = false
) {
console.log(`Updating page: ${pageId} in Notion API...`);
const notionProperties = {};
for (const [key, value] of Object.entries(properties)) {
if (key === "Email" && value != "") {
notionProperties[key] = { email: value };
} else if (key === "Name") {
notionProperties[key] = { title: [{ text: { content: value } }] };
} else if (key === "Features") {
notionProperties[key] = {
multi_select: value.map((feature) => ({
name: feature,
})),
};
} else if (typeof value === "string" && value != "") {
notionProperties[key] = {
rich_text: [{ text: { content: value } }],
};
} else if (typeof value === "number" && value != undefined) {
notionProperties[key] = { number: value };
} else if (value && typeof value === "object" && !Array.isArray(value)) {
notionProperties[key] = value;
}
}
try {
const result = await notionFetch(`/pages/${pageId}`, {
method: "PATCH",
body: JSON.stringify({
properties: notionProperties,
archived: archive,
in_trash: trash,
}),
});
return result;
} catch (error) {
console.error(`Error updating Notion page ${pageId}:`, error);
throw error;
}
}
export async function getNotionBlocks(pageId, queryParams = {}) {
console.log(`Fetching blocks for page: ${pageId} from Notion API...`);
try {
const result = await notionFetch(
`/blocks/${pageId}/children?${new URLSearchParams(queryParams)}`
);
return result.results;
} catch (error) {
console.error(`Error fetching Notion page ${pageId}:`, error);
throw error;
}
}
export async function getNotionSVGIcon(url) {
try {
console.log(`Fetching SVG icon from: ${url}`);
const response = await fetch(url);
if (!response.ok)
throw new Error(`Failed to fetch SVG: ${response.statusText}`);
let svgText = await response.text();
iconColorSubstitutions.forEach(([searchColor, replaceColor]) => {
svgText = svgText.replaceAll(searchColor, replaceColor);
});
return svgText;
} catch (error) {
console.error(error);
return null;
}
}
// Transform Notion blocks to content format
export async function transformContent(env, pageId) {
try {
console.log(`Transforming content for page: ${pageId}`);
const navigationItems = await getNavigation(env);
// Fetch blocks from Notion
const blocks = await getNotionBlocks(pageId);
const content = [];
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
const blockType = block.type;
// Group consecutive list items
if (
blockType === "bulleted_list_item" ||
blockType === "numbered_list_item"
) {
const isNumbered = blockType === "numbered_list_item";
const children = [];
// Collect consecutive list items of the same type
while (
i < blocks.length &&
blocks[i].type ===
(isNumbered ? "numbered_list_item" : "bulleted_list_item")
) {
children.push({
type: "listItem",
text: await extractRichText(
isNumbered
? blocks[i].numbered_list_item.rich_text
: blocks[i].bulleted_list_item.rich_text,
navigationItems
),
});
i++;
}
// Step back one index because the for loop will increment it
i--;
content.push({
type: "list",
ordered: isNumbered,
children,
});
continue;
}
switch (blockType) {
case "heading_1":
content.push({
type: "title1",
text: await extractRichText(
block.heading_1.rich_text,
navigationItems
),
});
break;
case "heading_2":
content.push({
type: "title2",
text: await extractRichText(
block.heading_2.rich_text,
navigationItems
),
});
break;
case "heading_3":
content.push({
type: "title3",
text: await extractRichText(
block.heading_3.rich_text,
navigationItems
),
});
break;
case "paragraph":
const paragraphText = await extractRichText(
block.paragraph.rich_text,
navigationItems
);
if (paragraphText.trim()) {
content.push({
type: "paragraph",
text: paragraphText,
});
}
break;
case "divider":
content.push({
type: "divider",
});
break;
case "image":
content.push({
type: "image",
url: block.image.external?.url || block.image.file?.url,
caption: block.image.caption
? await extractRichText(block.image.caption, navigationItems)
: null,
});
break;
case "quote":
content.push({
type: "quote",
text: await extractRichText(block.quote.rich_text, navigationItems),
});
break;
case "code":
content.push({
type: "code",
text: await extractRichText(block.code.rich_text, navigationItems),
language: block.code.language,
});
break;
case "bookmark":
content.push({
type: "button",
text: await extractRichText(
block.bookmark.caption,
navigationItems
),
url: block.bookmark.url,
});
break;
case "callout":
content.push({
type: "callout",
text: await extractRichText(
block.callout.rich_text,
navigationItems
),
icon: block.callout.icon?.emoji || null,
});
break;
case "link_to_page":
const linkedNavigationItem = getItemByNotionId(
navigationItems,
block.link_to_page.page_id
);
content.push({
type: "button",
url: `/${linkedNavigationItem.slug}`,
text: linkedNavigationItem.name,
icon: linkedNavigationItem.icon,
});
break;
default:
// For unsupported block types, try to extract text if available
console.log(`Unsupported block type: ${blockType}`);
console.log(JSON.stringify(block));
break;
}
}
return content;
} catch (error) {
console.error(`Error transforming page content for ${pageId}:`, error);
// Return fallback content if transformation fails
return [
{
type: "paragraph",
text: "Content could not be loaded at this time.",
},
];
}
}
function processLink(text, href, navigationItems) {
const icon = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<path d="M56.855,1.184L56.816,1.184L9.356,1.184C6.044,1.184 3.356,3.872 3.356,7.184C3.356,10.495 6.044,13.184 9.356,13.184L42.331,13.184L2.941,52.574C0.599,54.915 0.599,58.718 2.941,61.059C5.282,63.401 9.085,63.401 11.426,61.059L50.816,21.669L50.816,54.644C50.816,57.956 53.505,60.644 56.816,60.644C60.128,60.644 62.816,57.956 62.816,54.644L62.816,7.184C62.816,6.374 62.656,5.601 62.364,4.896L62.357,4.879C62.065,4.174 61.632,3.514 61.059,2.941C60.486,2.368 59.826,1.935 59.121,1.643L59.104,1.636C58.827,1.521 58.539,1.427 58.243,1.354L58.204,1.345L58.166,1.336L58.128,1.328L58.09,1.319L58.052,1.311L58.014,1.303L57.988,1.298L57.952,1.291L57.915,1.284L57.878,1.277L57.841,1.271L57.804,1.265L57.767,1.259L57.73,1.253L57.692,1.247L57.655,1.242L57.618,1.237L57.58,1.232L57.543,1.227L57.505,1.223L57.467,1.218L57.43,1.215L57.392,1.211L57.354,1.207L57.316,1.204L57.278,1.201L57.24,1.198L57.202,1.196L57.163,1.193L57.125,1.191L57.087,1.19L57.048,1.188L57.01,1.187L56.971,1.185L56.933,1.185L56.894,1.184L56.855,1.184Z"/>
</g>
</svg>
`;
var pageIdNoDashes = null;
if (href.startsWith("/")) {
pageIdNoDashes = href.split("/").pop().replaceAll("#", "");
}
if (href.includes("notion.so/")) {
if (href.includes("-")) {
pageIdNoDashes = href.split("-").pop().replaceAll("#", "");
} else {
pageIdNoDashes = href.split("/").pop().replaceAll("#", "");
}
}
const textSpan = `<span class="th-link-text">${text}</span>`;
const iconSpan = `<span class="th-link-icon">${icon}</span>`;
if (pageIdNoDashes != null) {
const linkedNavigationItem = getItemByNotionId(
navigationItems,
pageIdNoDashes
);
return `<a href="/${linkedNavigationItem.slug}" class="th-link">${textSpan}</a>`;
} else {
return `<a href="${href}" class="th-link">${textSpan}${iconSpan}</a>`;
}
}
// Helper function to extract rich text content
export async function extractRichText(richTextArray, navigationItems) {
if (!richTextArray || !Array.isArray(richTextArray)) {
return "";
}
const textPieces = await Promise.all(
richTextArray.map(async (textObj) => {
// Handle custom emoji mentions
if (
textObj.type === "mention" &&
textObj.mention?.type === "custom_emoji" &&
textObj.mention.custom_emoji?.url
) {
const emoji = await processNotionIcon(
textObj.mention.custom_emoji,
"th-inline-emoji"
);
return emoji;
}
let text = textObj.plain_text || "";
// Handle formatting
if (textObj.annotations) {
const annotations = textObj.annotations;
if (annotations.bold) {
text = `<strong>${text}</strong>`;
}
if (annotations.italic) {
text = `<i>${text}</i>`;
}
if (annotations.underline) {
text = `<u>${text}</u>`;
}
if (annotations.strikethrough) {
text = `<s>${text}</s>`;
}
if (annotations.code) {
text = `<code>${text}</code>`;
}
}
// Handle links
if (textObj.href) {
text = processLink(text, textObj.href, navigationItems);
}
return text;
})
);
return textPieces.join("");
}
// Utility: safely extract plain text from a Notion title or rich_text field
export function getPlainText(field) {
if (!field) return "";
if (field.type === "title" || field.type === "rich_text") {
return field[field.type].map((t) => t.plain_text).join("") || "";
}
return "";
}
// Utility: convert a string to camelCase
export function toCamelCase(str = "") {
return str
.replace(/[^a-zA-Z0-9 ]/g, " ") // remove non-alphanumeric except spaces
.trim()
.split(/\s+/) // split on spaces
.map((word, index) => {
if (index === 0) return word.toLowerCase();
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
})
.join("");
}
// Moved from properties.js: buildNewCache, now buildListCache
export function buildListCache(
cachedList,
{ added = [], updated = [], deleted = [] }
) {
// Remove deleted and items that will be updated
const filtered = cachedList.filter(
(p) =>
!deleted.some((d) => d.notionId === p.notionId) &&
!updated.some((u) => u.notionId === p.notionId)
);
return [...filtered, ...updated, ...added];
}
export function getItemByNotionId(items, notionId) {
const itemWithDashes = items.find((item) => item.notionId === notionId);
if (itemWithDashes) {
return itemWithDashes;
}
const itemWithoutDashes = items.find(
(item) => item.notionId.replaceAll("-", "") === notionId.replaceAll("-", "")
);
if (itemWithoutDashes) {
return itemWithoutDashes;
}
return null;
}