diff options
| author | Fuwn <[email protected]> | 2026-02-11 23:43:28 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-11 23:43:31 -0800 |
| commit | 17475e06c8822c854dcfa1335f44957b6a3eb629 (patch) | |
| tree | 4e85234cf29e54ef747f1dc01ad2c523f18bd692 /src/notion.rs | |
| parent | chore: Update CI references to updated Rust toolchain channel (diff) | |
| download | locus-17475e06c8822c854dcfa1335f44957b6a3eb629.tar.xz locus-17475e06c8822c854dcfa1335f44957b6a3eb629.zip | |
feat: Replace static blog system with Notion-backed dynamic content
Diffstat (limited to 'src/notion.rs')
| -rw-r--r-- | src/notion.rs | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/src/notion.rs b/src/notion.rs new file mode 100644 index 0000000..74bde89 --- /dev/null +++ b/src/notion.rs @@ -0,0 +1,207 @@ +use serde::Deserialize; + +const NOTION_API_VERSION: &str = "2022-06-28"; + +#[derive(Deserialize)] +pub struct QueryResponse { + pub results: Vec<Page>, + pub has_more: bool, +} + +#[derive(Deserialize)] +pub struct Page { + pub id: String, + pub properties: serde_json::Value, +} + +#[derive(Deserialize)] +pub struct BlockChildrenResponse { + pub results: Vec<Block>, + pub has_more: bool, +} + +#[derive(Deserialize)] +pub struct Block { + #[serde(rename = "type")] + pub kind: String, + #[serde(flatten)] + pub content: serde_json::Value, + pub id: String, +} + +pub fn extract_title(properties: &serde_json::Value, field: &str) -> String { + properties[field]["title"] + .as_array() + .and_then(|rich_text_array| rich_text_array.first()) + .and_then(|rich_text_element| rich_text_element["plain_text"].as_str()) + .unwrap_or("") + .to_string() +} + +pub fn extract_rich_text( + properties: &serde_json::Value, + field: &str, +) -> String { + properties[field]["rich_text"] + .as_array() + .map(|rich_text_array| { + rich_text_array + .iter() + .filter_map(|rich_text_element| { + rich_text_element["plain_text"].as_str() + }) + .collect::<Vec<_>>() + .join("") + }) + .unwrap_or_default() +} + +pub fn extract_number( + properties: &serde_json::Value, + field: &str, +) -> Option<u8> { + properties[field]["number"] + .as_u64() + .and_then(|number_value| u8::try_from(number_value).ok()) +} + +pub fn extract_date(properties: &serde_json::Value, field: &str) -> String { + properties[field]["date"]["start"].as_str().unwrap_or("").to_string() +} + +pub fn extract_relation_ids( + properties: &serde_json::Value, + field: &str, +) -> Vec<String> { + properties[field]["relation"] + .as_array() + .map(|relation_array| { + relation_array + .iter() + .filter_map(|relation_entry| { + relation_entry["id"].as_str().map(String::from) + }) + .collect() + }) + .unwrap_or_default() +} + +pub fn format_notion_date(iso_date: &str) -> String { + if iso_date.len() < 10 { + return iso_date.to_string(); + } + + let year = &iso_date[0..4]; + let month = &iso_date[5..7]; + let day = &iso_date[8..10]; + + format!("{year}. {month}. {day}.") +} + +pub fn extract_block_plain_text(block: &Block) -> String { + let block_content = &block.content[&block.kind]; + + block_content["rich_text"] + .as_array() + .map(|rich_text_array| { + rich_text_array + .iter() + .filter_map(|rich_text_element| { + rich_text_element["plain_text"].as_str() + }) + .collect::<Vec<_>>() + .join("") + }) + .unwrap_or_default() +} + +pub fn query_database( + http_client: &reqwest::blocking::Client, + api_key: &str, + database_identifier: &str, +) -> Result<Vec<Page>, reqwest::Error> { + let mut all_pages = Vec::new(); + let mut start_cursor: Option<String> = None; + + loop { + let mut request_body = serde_json::json!({}); + + if let Some(ref cursor) = start_cursor { + request_body["start_cursor"] = serde_json::json!(cursor); + } + + let response = http_client + .post(format!( + "https://api.notion.com/v1/databases/{database_identifier}/query" + )) + .header("Authorization", format!("Bearer {api_key}")) + .header("Notion-Version", NOTION_API_VERSION) + .json(&request_body) + .send()? + .json::<QueryResponse>()?; + let has_more = response.has_more; + + all_pages.extend(response.results); + + if !has_more { + break; + } + + start_cursor = all_pages.last().map(|last_page| last_page.id.clone()); + } + + Ok(all_pages) +} + +pub fn fetch_page_content( + http_client: &reqwest::blocking::Client, + api_key: &str, + page_identifier: &str, +) -> Result<String, reqwest::Error> { + let mut all_lines = Vec::new(); + let mut start_cursor: Option<String> = None; + + loop { + let mut request_url = format!( + "https://api.notion.com/v1/blocks/{page_identifier}/children?page_size=100" + ); + + if let Some(ref cursor) = start_cursor { + request_url.push_str(&format!("&start_cursor={cursor}")); + } + + let response = http_client + .get(&request_url) + .header("Authorization", format!("Bearer {api_key}")) + .header("Notion-Version", NOTION_API_VERSION) + .send()? + .json::<BlockChildrenResponse>()?; + let has_more = response.has_more; + + for block in &response.results { + let line = match block.kind.as_str() { + "heading_1" => format!("# {}", extract_block_plain_text(block)), + "heading_2" => format!("## {}", extract_block_plain_text(block)), + "heading_3" => format!("### {}", extract_block_plain_text(block)), + "bulleted_list_item" | "numbered_list_item" => { + format!("* {}", extract_block_plain_text(block)) + } + "quote" => format!("> {}", extract_block_plain_text(block)), + "code" => format!("```\n{}\n```", extract_block_plain_text(block)), + "divider" => "---".to_string(), + _ => extract_block_plain_text(block), + }; + + all_lines.push(line); + } + + if !has_more { + break; + } + + start_cursor = + response.results.last().map(|last_block| last_block.id.clone()); + } + + Ok(all_lines.join("\n")) +} |