elmethis_notion/
client.rs

1#[derive(Debug)]
2pub struct Client {
3    pub notionrs_client: notionrs::client::Client,
4}
5
6#[derive(Debug)]
7pub struct Image {
8    pub src: String,
9    pub id: String,
10}
11
12impl Client {
13    pub fn new<T>(secret: T) -> Self
14    where
15        T: AsRef<str>,
16    {
17        Client {
18            notionrs_client: notionrs::client::Client::new().secret(secret),
19        }
20    }
21
22    #[async_recursion::async_recursion]
23    pub async fn convert_block(
24        &mut self,
25        page_id: &str,
26    ) -> Result<Vec<crate::block::Block>, crate::error::Error> {
27        let mut blocks: Vec<crate::block::Block> = Vec::new();
28
29        let results = self
30            .notionrs_client
31            .get_block_children_all()
32            .block_id(page_id)
33            .send()
34            .await?;
35
36        for block in results {
37            match block.block {
38                notionrs::object::block::Block::Audio { audio: _ } => {}
39                notionrs::object::block::Block::Bookmark { bookmark } => {
40                    let mut props = crate::block::ElmBookmarkProps {
41                        url: bookmark.url.clone(),
42                        margin: "2rem".to_string(),
43                        ..Default::default()
44                    };
45
46                    let response = reqwest::Client::new()
47                        .get(&bookmark.url)
48                        .header("user-agent", "Rust - reqwest")
49                        .send()
50                        .await?
51                        .text()
52                        .await?;
53
54                    let document = scraper::Html::parse_document(&response);
55
56                    // title
57
58                    let title = document
59                        .select(&scraper::Selector::parse("title")?)
60                        .next()
61                        .map(|element| element.text().collect::<String>());
62
63                    let og_title_selector = scraper::Selector::parse("meta[property='og:title']")?;
64
65                    if let Some(element) = document.select(&og_title_selector).next() {
66                        if let Some(content) = element.value().attr("content") {
67                            props.title = Some(content.to_string());
68                        }
69                    }
70
71                    if let Some(title) = title {
72                        props.title = Some(title);
73                    }
74
75                    // description
76
77                    let description = document
78                        .select(&scraper::Selector::parse("meta[name='description']")?)
79                        .next()
80                        .map(|element| element.value().attr("content").unwrap().to_string());
81
82                    if let Some(description) = description {
83                        props.description = Some(description);
84                    }
85
86                    let og_description_selector =
87                        scraper::Selector::parse("meta[property='og:description']")?;
88
89                    if let Some(element) = document.select(&og_description_selector).next() {
90                        if let Some(content) = element.value().attr("content") {
91                            props.description = Some(content.to_string());
92                        }
93                    }
94
95                    let og_image_selector = scraper::Selector::parse("meta[property='og:image']")?;
96
97                    if let Some(element) = document.select(&og_image_selector).next() {
98                        if let Some(content) = element.value().attr("content") {
99                            props.image = Some(content.to_string());
100                        }
101                    }
102
103                    let block = crate::block::Block::ElmBookmark(crate::block::ElmBookmark {
104                        props,
105                        id: block.id,
106                    });
107
108                    blocks.push(block);
109                }
110                notionrs::object::block::Block::Breadcrumb { breadcrumb: _ } => {}
111                notionrs::object::block::Block::BulletedListItem { bulleted_list_item } => {
112                    let mut list_item_children: Vec<crate::block::Block> = Vec::new();
113
114                    let rich_text_block = Client::convert_rich_text(bulleted_list_item.rich_text);
115                    list_item_children.extend(rich_text_block);
116
117                    if block.has_children {
118                        let list_item_children_blocks = self.convert_block(&block.id).await?;
119                        list_item_children.extend(list_item_children_blocks);
120                    }
121
122                    let list_item_block =
123                        crate::block::Block::ElmListItem(crate::block::ElmListItem {
124                            children: list_item_children,
125                            id: block.id.clone(),
126                        });
127
128                    let last_item = blocks.last_mut();
129
130                    match last_item {
131                        Some(crate::block::Block::ElmBulletedList(elm_bulleted_list)) => {
132                            elm_bulleted_list.children.push(list_item_block);
133                        }
134                        Some(_) | None => {
135                            let new_ul = vec![list_item_block];
136                            blocks.push(crate::block::Block::ElmBulletedList(
137                                crate::block::ElmBulletedList {
138                                    children: new_ul,
139                                    id: block.id,
140                                },
141                            ));
142                        }
143                    };
144                }
145                notionrs::object::block::Block::Callout { callout } => {
146                    let mut children: Vec<crate::block::Block> = Vec::new();
147                    let text_blocks = Client::convert_rich_text(callout.rich_text);
148                    let children_blocks = self.convert_block(&block.id).await?;
149                    children.extend(text_blocks);
150                    children.extend(children_blocks);
151
152                    let r#type = match callout.color {
153                        notionrs::object::color::Color::Default
154                        | notionrs::object::color::Color::DefaultBackground
155                        | notionrs::object::color::Color::Blue
156                        | notionrs::object::color::Color::BlueBackground
157                        | notionrs::object::color::Color::Gray
158                        | notionrs::object::color::Color::GrayBackground => "note",
159                        notionrs::object::color::Color::Green
160                        | notionrs::object::color::Color::GreenBackground => "tip",
161                        notionrs::object::color::Color::Purple
162                        | notionrs::object::color::Color::PurpleBackground => "important",
163                        notionrs::object::color::Color::Yellow
164                        | notionrs::object::color::Color::YellowBackground
165                        | notionrs::object::color::Color::Orange
166                        | notionrs::object::color::Color::OrangeBackground
167                        | notionrs::object::color::Color::Brown
168                        | notionrs::object::color::Color::BrownBackground => "warning",
169                        notionrs::object::color::Color::Red
170                        | notionrs::object::color::Color::RedBackground
171                        | notionrs::object::color::Color::Pink
172                        | notionrs::object::color::Color::PinkBackground => "caution",
173                    }
174                    .to_string();
175
176                    let props = crate::block::ElmCalloutProps { r#type };
177
178                    let callout_block = crate::block::Block::ElmCallout(crate::block::ElmCallout {
179                        props,
180                        children,
181                        id: block.id,
182                    });
183                    blocks.push(callout_block);
184                }
185                notionrs::object::block::Block::ChildDatabase { child_database: _ } => {}
186                notionrs::object::block::Block::ChildPage { child_page: _ } => {}
187                notionrs::object::block::Block::Code { code } => {
188                    let language = code.language.to_string();
189
190                    let caption = code
191                        .caption
192                        .iter()
193                        .map(|t| t.to_string())
194                        .collect::<String>();
195
196                    let props = crate::block::ElmCodeBlockProps {
197                        code: code
198                            .rich_text
199                            .iter()
200                            .map(|t| t.to_string())
201                            .collect::<String>(),
202                        language: language.to_string(),
203                        caption: if caption.is_empty() {
204                            language.to_string()
205                        } else {
206                            caption
207                        },
208                        margin: "2rem".to_string(),
209                    };
210
211                    blocks.push(crate::block::Block::ElmCodeBlock(
212                        crate::block::ElmCodeBlock {
213                            props,
214                            id: block.id,
215                        },
216                    ));
217                }
218                notionrs::object::block::Block::ColumnList { column_list: _ } => {
219                    let children = self.convert_block(&block.id).await?;
220                    let block = crate::block::Block::ElmColumnList(crate::block::ElmColumnList {
221                        children,
222                        id: block.id,
223                    });
224                    blocks.push(block);
225                }
226                notionrs::object::block::Block::Column { column: _ } => {
227                    let children = self.convert_block(&block.id).await?;
228                    let block = crate::block::Block::ElmColumn(crate::block::ElmColumn {
229                        children,
230                        id: block.id,
231                    });
232                    blocks.push(block);
233                }
234                notionrs::object::block::Block::Divider { divider: _ } => {
235                    blocks.push(crate::block::Block::ElmDivider(crate::block::ElmDivider {
236                        props: crate::block::ElmDividerProps {
237                            margin: "2rem".to_string(),
238                        },
239                        id: block.id,
240                    }));
241                }
242                notionrs::object::block::Block::Embed { embed: _ } => {}
243                notionrs::object::block::Block::Equation { equation } => {
244                    let props = crate::block::ElmKatexProps {
245                        expression: equation.expression,
246                        block: true,
247                    };
248
249                    let block = crate::block::Block::ElmKatex(crate::block::ElmKatex {
250                        props,
251                        id: block.id,
252                    });
253
254                    blocks.push(block);
255                }
256                notionrs::object::block::Block::File { file } => {
257                    let (name, src) = match file {
258                        notionrs::object::file::File::External(f) => (f.name, f.external.url),
259                        notionrs::object::file::File::Uploaded(f) => (f.name, f.file.url),
260                    };
261
262                    let props = crate::block::ElmFileProps {
263                        name,
264                        src,
265                        margin: "2rem".to_string(),
266                    };
267
268                    let block = crate::block::Block::ElmFile(crate::block::ElmFile {
269                        props,
270                        id: block.id,
271                    });
272
273                    blocks.push(block);
274                }
275                notionrs::object::block::Block::Heading1 { heading_1 } => {
276                    let heading = heading_1;
277                    if heading.is_toggleable {
278                        let summary = heading
279                            .rich_text
280                            .iter()
281                            .map(|t| t.to_string())
282                            .collect::<String>();
283
284                        let props = crate::block::ElmToggleProps {
285                            summary,
286                            margin: "2rem".to_string(),
287                        };
288
289                        let children = self.convert_block(&block.id).await?;
290
291                        let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
292                            props,
293                            children,
294                            id: block.id,
295                        });
296
297                        blocks.push(block);
298                    } else {
299                        let props = crate::block::ElmHeadingProps {
300                            text: heading
301                                .rich_text
302                                .iter()
303                                .map(|t| t.to_string())
304                                .collect::<String>(),
305                        };
306
307                        let block = crate::block::Block::ElmHeading1(crate::block::ElmHeading1 {
308                            props,
309                            id: block.id,
310                        });
311
312                        blocks.push(block);
313                    }
314                }
315                notionrs::object::block::Block::Heading2 { heading_2 } => {
316                    let heading = heading_2;
317                    if heading.is_toggleable {
318                        let summary = heading
319                            .rich_text
320                            .iter()
321                            .map(|t| t.to_string())
322                            .collect::<String>();
323
324                        let props = crate::block::ElmToggleProps {
325                            summary,
326                            margin: "2rem".to_string(),
327                        };
328
329                        let children = self.convert_block(&block.id).await?;
330
331                        let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
332                            props,
333                            children,
334                            id: block.id,
335                        });
336
337                        blocks.push(block);
338                    } else {
339                        let props = crate::block::ElmHeadingProps {
340                            text: heading
341                                .rich_text
342                                .iter()
343                                .map(|t| t.to_string())
344                                .collect::<String>(),
345                        };
346
347                        let block = crate::block::Block::ElmHeading1(crate::block::ElmHeading1 {
348                            props,
349                            id: block.id,
350                        });
351
352                        blocks.push(block);
353                    }
354                }
355                notionrs::object::block::Block::Heading3 { heading_3 } => {
356                    let heading = heading_3;
357                    if heading.is_toggleable {
358                        let summary = heading
359                            .rich_text
360                            .iter()
361                            .map(|t| t.to_string())
362                            .collect::<String>();
363
364                        let props = crate::block::ElmToggleProps {
365                            summary,
366                            margin: "2rem".to_string(),
367                        };
368
369                        let children = self.convert_block(&block.id).await?;
370
371                        let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
372                            props,
373                            children,
374                            id: block.id,
375                        });
376
377                        blocks.push(block);
378                    } else {
379                        let props = crate::block::ElmHeadingProps {
380                            text: heading
381                                .rich_text
382                                .iter()
383                                .map(|t| t.to_string())
384                                .collect::<String>(),
385                        };
386
387                        let block = crate::block::Block::ElmHeading1(crate::block::ElmHeading1 {
388                            props,
389                            id: block.id,
390                        });
391
392                        blocks.push(block);
393                    }
394                }
395                notionrs::object::block::Block::Image { image } => {
396                    let (src, alt) = match image {
397                        notionrs::object::file::File::External(f) => (
398                            f.external.url,
399                            f.caption.map(|rich_text| {
400                                rich_text.iter().map(|t| t.to_string()).collect::<String>()
401                            }),
402                        ),
403                        notionrs::object::file::File::Uploaded(f) => (
404                            f.file.url,
405                            f.caption.map(|rich_text| {
406                                rich_text.iter().map(|t| t.to_string()).collect::<String>()
407                            }),
408                        ),
409                    };
410
411                    let props = crate::block::ElmImageProps {
412                        src: src.clone(),
413                        alt,
414                        enable_modal: true,
415                        margin: "2rem".to_string(),
416                    };
417
418                    let image_block = crate::block::Block::ElmImage(crate::block::ElmImage {
419                        props,
420                        id: block.id.clone(),
421                    });
422
423                    blocks.push(image_block);
424                }
425                notionrs::object::block::Block::LinkPreview { link_preview: _ } => {}
426                notionrs::object::block::Block::NumberedListItem { numbered_list_item } => {
427                    let mut list_item_children: Vec<crate::block::Block> = Vec::new();
428
429                    let rich_text_block = Client::convert_rich_text(numbered_list_item.rich_text);
430                    list_item_children.extend(rich_text_block);
431
432                    if block.has_children {
433                        let list_item_children_blocks = self.convert_block(&block.id).await?;
434                        list_item_children.extend(list_item_children_blocks);
435                    }
436
437                    let list_item_block =
438                        crate::block::Block::ElmListItem(crate::block::ElmListItem {
439                            children: list_item_children,
440                            id: block.id.clone(),
441                        });
442
443                    let last_item = blocks.last_mut();
444
445                    match last_item {
446                        Some(crate::block::Block::ElmNumberedList(elm_numbered_list)) => {
447                            elm_numbered_list.children.push(list_item_block);
448                        }
449                        Some(_) | None => {
450                            let new_ol = vec![list_item_block];
451                            blocks.push(crate::block::Block::ElmNumberedList(
452                                crate::block::ElmNumberedList {
453                                    children: new_ol,
454                                    id: block.id,
455                                },
456                            ));
457                        }
458                    };
459                }
460                notionrs::object::block::Block::Paragraph { paragraph } => {
461                    let block = crate::block::Block::ElmParagraph(crate::block::ElmParagraph {
462                        children: Client::convert_rich_text(paragraph.rich_text),
463                        id: block.id,
464                    });
465
466                    blocks.push(block);
467                }
468                notionrs::object::block::Block::Pdf { pdf: _ } => {}
469                notionrs::object::block::Block::Quote { quote } => {
470                    let mut children = Vec::new();
471
472                    let inline_text_block = Client::convert_rich_text(quote.rich_text);
473
474                    let children_block = self.convert_block(&block.id).await?;
475
476                    children.extend(inline_text_block);
477                    children.extend(children_block);
478
479                    let block = crate::block::Block::ElmBlockQuote(crate::block::ElmBlockQuote {
480                        children,
481                        id: block.id,
482                    });
483
484                    blocks.push(block);
485                }
486                notionrs::object::block::Block::SyncedBlock { synced_block: _ } => {
487                    let children = self.convert_block(&block.id).await?;
488                    blocks.extend(children);
489                }
490                notionrs::object::block::Block::Table { table: _ } => {
491                    let rows = self
492                        .notionrs_client
493                        .get_block_children_all()
494                        .block_id(block.id)
495                        .send()
496                        .await?;
497
498                    if let Some((header_row, body_rows)) = rows.split_first() {
499                        let table_header_block =
500                            if let notionrs::object::block::Block::TableRow { table_row } =
501                                &header_row.block
502                            {
503                                let cells_blocks = table_row
504                                    .cells
505                                    .iter()
506                                    .map(|cell| {
507                                        crate::block::Block::ElmTableCell(
508                                            crate::block::ElmTableCell {
509                                                props: crate::block::ElmTableCellProps {
510                                                    has_header: true,
511                                                    text: cell
512                                                        .iter()
513                                                        .map(|t| t.to_string())
514                                                        .collect::<String>(),
515                                                },
516                                            },
517                                        )
518                                    })
519                                    .collect::<Vec<crate::block::Block>>();
520
521                                let table_row_block =
522                                    crate::block::Block::ElmTableRow(crate::block::ElmTableRow {
523                                        children: cells_blocks,
524                                    });
525
526                                crate::block::Block::ElmTableHeader(crate::block::ElmTableHeader {
527                                    children: vec![table_row_block],
528                                })
529                            } else {
530                                crate::block::Block::ElmTableHeader(crate::block::ElmTableHeader {
531                                    children: vec![],
532                                })
533                            };
534
535                        let table_body_row_blocks =
536                            body_rows.iter().filter_map(|row| match &row.block {
537                                notionrs::object::block::Block::TableRow { table_row } => {
538                                    let cells_blocks = table_row
539                                        .cells
540                                        .iter()
541                                        .map(|cell| {
542                                            crate::block::Block::ElmTableCell(
543                                                crate::block::ElmTableCell {
544                                                    props: crate::block::ElmTableCellProps {
545                                                        has_header: false,
546                                                        text: cell
547                                                            .iter()
548                                                            .map(|t| t.to_string())
549                                                            .collect::<String>(),
550                                                    },
551                                                },
552                                            )
553                                        })
554                                        .collect::<Vec<crate::block::Block>>();
555
556                                    Some(crate::block::Block::ElmTableRow(
557                                        crate::block::ElmTableRow {
558                                            children: cells_blocks,
559                                        },
560                                    ))
561                                }
562                                _ => None,
563                            });
564
565                        let table_body_block =
566                            crate::block::Block::ElmTableBody(crate::block::ElmTableBody {
567                                children: table_body_row_blocks.collect(),
568                            });
569
570                        let table_block = crate::block::Block::ElmTable(crate::block::ElmTable {
571                            children: vec![table_header_block, table_body_block],
572                        });
573
574                        blocks.push(table_block);
575                    }
576                }
577                notionrs::object::block::Block::TableRow { table_row: _ } => {}
578                notionrs::object::block::Block::Template { template: _ } => {}
579                notionrs::object::block::Block::ToDo { to_do } => {
580                    let props = crate::block::ElmCheckboxProps {
581                        label: to_do
582                            .rich_text
583                            .iter()
584                            .map(|t| t.to_string())
585                            .collect::<String>(),
586                    };
587
588                    let block = crate::block::Block::ElmCheckbox(crate::block::ElmCheckbox {
589                        props,
590                        id: block.id,
591                    });
592
593                    blocks.push(block);
594                }
595                notionrs::object::block::Block::Toggle { toggle } => {
596                    let summary = toggle
597                        .rich_text
598                        .iter()
599                        .map(|t| t.to_string())
600                        .collect::<String>();
601
602                    let props = crate::block::ElmToggleProps {
603                        summary,
604                        margin: "2rem".to_string(),
605                    };
606
607                    let children = self.convert_block(&block.id).await?;
608
609                    let block = crate::block::Block::ElmToggle(crate::block::ElmToggle {
610                        props,
611                        children,
612                        id: block.id,
613                    });
614
615                    blocks.push(block);
616                }
617                notionrs::object::block::Block::Video { video: _ } => {}
618                notionrs::object::block::Block::Unknown(_) => {}
619            };
620        }
621
622        Ok(blocks)
623    }
624
625    pub fn convert_rich_text(
626        rich_text: Vec<notionrs::object::rich_text::RichText>,
627    ) -> Vec<crate::block::Block> {
628        let blocks: Vec<crate::block::Block> = rich_text
629            .iter()
630            .map(|r| {
631                if let notionrs::object::rich_text::RichText::Mention { mention, .. } = r {
632                    if let notionrs::object::rich_text::mention::Mention::CustomEmoji {
633                        custom_emoji,
634                    } = mention
635                    {
636                        let props = crate::block::ElmInlineIconProps {
637                            src: custom_emoji.url.to_string(),
638                            alt: custom_emoji.name.to_string(),
639                        };
640
641                        let block =
642                            crate::block::Block::ElmInlineIcon(crate::block::ElmInlineIcon {
643                                id: custom_emoji.id.to_string(),
644                                props,
645                            });
646
647                        return block;
648                    }
649                }
650
651                let annotations = match r {
652                    notionrs::object::rich_text::RichText::Text { annotations, .. } => annotations,
653                    notionrs::object::rich_text::RichText::Mention { annotations, .. } => {
654                        annotations
655                    }
656                    notionrs::object::rich_text::RichText::Equation { annotations, .. } => {
657                        annotations
658                    }
659                };
660
661                let plain_text = match r {
662                    notionrs::object::rich_text::RichText::Text { plain_text, .. } => plain_text,
663                    notionrs::object::rich_text::RichText::Mention { plain_text, .. } => plain_text,
664                    notionrs::object::rich_text::RichText::Equation { plain_text, .. } => {
665                        plain_text
666                    }
667                };
668
669                let props = crate::block::ElmInlineTextProps {
670                    text: plain_text.to_string(),
671                    bold: annotations.bold,
672                    italic: annotations.italic,
673                    underline: annotations.underline,
674                    strikethrough: annotations.strikethrough,
675                    code: annotations.code,
676                    color: match annotations.color {
677                        notionrs::object::color::Color::Default => None,
678                        notionrs::object::color::Color::Blue => Some(String::from("#6987b8")),
679                        notionrs::object::color::Color::Brown => Some(String::from("#8b4c3f")),
680                        notionrs::object::color::Color::Gray => Some(String::from("#868e9c")),
681                        notionrs::object::color::Color::Green => Some(String::from("#59b57c")),
682                        notionrs::object::color::Color::Orange => Some(String::from("#bf7e71")),
683                        notionrs::object::color::Color::Pink => Some(String::from("#c9699e")),
684                        notionrs::object::color::Color::Purple => Some(String::from("#9771bd")),
685                        notionrs::object::color::Color::Red => Some(String::from("#b36472")),
686                        notionrs::object::color::Color::Yellow => Some(String::from("#b8a36e")),
687                        _ => None,
688                    },
689                };
690                crate::block::Block::ElmInlineText(crate::block::ElmInlineText { props })
691            })
692            .collect();
693
694        blocks
695    }
696}