aboutsummaryrefslogtreecommitdiff
path: root/src/notion.rs
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-11 23:43:28 -0800
committerFuwn <[email protected]>2026-02-11 23:43:31 -0800
commit17475e06c8822c854dcfa1335f44957b6a3eb629 (patch)
tree4e85234cf29e54ef747f1dc01ad2c523f18bd692 /src/notion.rs
parentchore: Update CI references to updated Rust toolchain channel (diff)
downloadlocus-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.rs207
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"))
+}