From 6c7c1842f8af09e1e5f8c4f26c498716c2096235 Mon Sep 17 00:00:00 2001 From: Kris Hansen Date: Wed, 11 Mar 2026 18:25:35 -0600 Subject: [PATCH 1/3] feat: Add support for all block types Add new 'blocks' subcommand for working with all Notion block types while maintaining backward compatibility with existing to-do commands. New commands: - notioncli blocks list List all blocks on the page - notioncli blocks list --type X Filter by block type - notioncli blocks add "text" Add a paragraph (default) - notioncli blocks add "text" -t X Add block of type X - notioncli blocks delete N Delete block by index Supported block types: - paragraph, heading_1/2/3 - bulleted_list_item, numbered_list_item - to_do, toggle, quote, callout - divider, code Changes: - Add BlockTypeInfo and SupportedBlockTypes map - Add RichTextBlock struct for parsing various block types - Add GetAllBlocks() with optional type filter - Add GetBlockContent() for any block type - Add GetBlockIcon() with special to-do handling - Add FormatAllBlocks() for formatted output - Add AddBlock() for adding any block type - Add DeleteBlock() for deleting any block - Add cmd/blocks.go with list/add/delete subcommands - Add 8 new tests for block type functions - Update README with blocks command documentation --- README.md | 50 ++++++- cmd/blocks.go | 178 ++++++++++++++++++++++++ utils/block.go | 326 +++++++++++++++++++++++++++++++++++++++++++- utils/block_test.go | 144 +++++++++++++++++++ 4 files changed, 684 insertions(+), 14 deletions(-) create mode 100644 cmd/blocks.go diff --git a/README.md b/README.md index 915951a..4f59f8b 100644 --- a/README.md +++ b/README.md @@ -36,19 +36,55 @@ You can interact with the tool using the built binary: ./notioncli [command] ``` -Here are the available commands: +### Task Commands (To-Do focused) + +- `list`: List all to-do tasks on the Notion page. +- `add `: Add a new to-do task to the Notion page. +- `check `: Mark a task as complete. +- `uncheck `: Mark a task as incomplete. +- `delete `: Delete a to-do task from the Notion page. + +### Block Commands (All block types) + +The `blocks` subcommand allows you to work with all Notion block types: + +```bash +# List all blocks +notioncli blocks list + +# List only specific block types +notioncli blocks list --type heading_1 + +# Add different block types +notioncli blocks add "Hello world" # paragraph (default) +notioncli blocks add "Section Title" -t heading_1 # heading +notioncli blocks add "Buy milk" -t to_do # to-do item +notioncli blocks add "Important note" -t callout # callout +notioncli blocks add "" -t divider # divider + +# Delete any block by index +notioncli blocks delete 5 +``` + +**Supported block types:** +- `paragraph` - Regular text +- `heading_1`, `heading_2`, `heading_3` - Headings +- `bulleted_list_item`, `numbered_list_item` - List items +- `to_do` - Checkbox items +- `toggle` - Collapsible content +- `quote` - Block quotes +- `callout` - Highlighted callouts +- `divider` - Horizontal dividers +- `code` - Code blocks + +### Other Commands -- `list`: List all tasks on the Notion page. -- `add`: Add a new task to the Notion page. -- `check`: Mark a task as complete. -- `uncheck`: Mark a task as incomplete. -- `delete`: Delete a task from the Notion page. - `completion`: Generate the autocompletion script for your shell - `help`: Show help information. ## Known Limitations -Currently, the tool only supports a single Notion page at a time and is focused on the ToDo use case. +Currently, the tool only supports a single Notion page at a time. ## Testing diff --git a/cmd/blocks.go b/cmd/blocks.go new file mode 100644 index 0000000..c03bbe4 --- /dev/null +++ b/cmd/blocks.go @@ -0,0 +1,178 @@ +// This code is licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package cmd + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "notioncli/utils" +) + +var blockType string + +// blocksCmd represents the blocks command +var blocksCmd = &cobra.Command{ + Use: "blocks", + Short: "Manage all block types on a Notion page", + Long: `The blocks command allows you to work with all Notion block types, +not just to-do items. Supported block types: + + paragraph, heading_1, heading_2, heading_3, + bulleted_list_item, numbered_list_item, to_do, + toggle, quote, callout, divider, code + +Examples: + notioncli blocks list # List all blocks + notioncli blocks list --type to_do # List only to-do blocks + notioncli blocks add "Hello" # Add a paragraph + notioncli blocks add "Title" -t heading_1 # Add a heading + notioncli blocks delete 5 # Delete block at index 5`, +} + +// blocksListCmd lists all blocks +var blocksListCmd = &cobra.Command{ + Use: "list", + Short: "List all blocks on the page", + Long: `List all blocks on the Notion page with their type and content.`, + Run: func(cmd *cobra.Command, args []string) { + notionAPIKey, pageID := utils.SetAPIConfig() + localTimezone, err := utils.GetLocalTimeZone() + if err != nil { + color.Red("Error getting timezone: %v", err) + return + } + + formatted, typeCounts, err := utils.FormatAllBlocks( + notionAPIKey, + pageID, + localTimezone, + blockType, + ) + if err != nil { + color.Red("Error: %v", err) + return + } + + if len(formatted) == 0 { + if blockType != "" { + color.Yellow("No blocks of type '%s' found.", blockType) + } else { + color.Yellow("No blocks found on this page.") + } + return + } + + fmt.Println() + for _, line := range formatted { + fmt.Println(line) + } + fmt.Println() + + // Print summary + var summary []string + for t, count := range typeCounts { + summary = append(summary, fmt.Sprintf("%d %s", count, t)) + } + sort.Strings(summary) + + total := len(formatted) + color.Cyan(" %d blocks: %s\n", total, strings.Join(summary, ", ")) + }, +} + +// blocksAddCmd adds a new block +var blocksAddCmd = &cobra.Command{ + Use: "add [text]", + Short: "Add a new block to the page", + Long: `Add a new block of the specified type. Default type is 'paragraph'. + +Supported types: + paragraph, heading_1, heading_2, heading_3, + bulleted_list_item, numbered_list_item, to_do, + toggle, quote, callout, divider, code + +Examples: + notioncli blocks add "Hello world" # Add paragraph + notioncli blocks add "Section Title" -t heading_1 + notioncli blocks add "" -t divider # Add divider (no text needed) + notioncli blocks add "Buy milk" -t to_do`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + text := args[0] + + // Validate block type + if blockType == "" { + blockType = "paragraph" + } + + if !utils.IsValidBlockType(blockType) { + color.Red("Error: unsupported block type '%s'", blockType) + color.Yellow("Supported types: %s", strings.Join(utils.GetSupportedBlockTypeNames(), ", ")) + return + } + + notionAPIKey, pageID := utils.SetAPIConfig() + + err := utils.AddBlock(notionAPIKey, pageID, blockType, text) + if err != nil { + color.Red("Error adding block: %v", err) + return + } + + icon := utils.SupportedBlockTypes[blockType].Icon + if blockType == "divider" { + color.Green("Added %s divider", icon) + } else { + color.Green("Added %s %s: %s", icon, blockType, text) + } + }, +} + +// blocksDeleteCmd deletes a block by index +var blocksDeleteCmd = &cobra.Command{ + Use: "delete [number]", + Short: "Delete a block by its index number", + Long: `Delete any block from the page by its index number. +Use 'notioncli blocks list' to see block numbers. + +Example: + notioncli blocks delete 3`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + order, err := strconv.Atoi(args[0]) + if err != nil { + color.Red("Error: '%s' is not a valid number", args[0]) + return + } + + notionAPIKey, pageID := utils.SetAPIConfig() + + err = utils.DeleteBlock(notionAPIKey, pageID, order) + if err != nil { + color.Red("Error deleting block: %v", err) + return + } + + color.Green("Deleted block %d", order) + }, +} + +func init() { + rootCmd.AddCommand(blocksCmd) + blocksCmd.AddCommand(blocksListCmd) + blocksCmd.AddCommand(blocksAddCmd) + blocksCmd.AddCommand(blocksDeleteCmd) + + // Flags for list command + blocksListCmd.Flags().StringVarP(&blockType, "type", "t", "", "Filter by block type") + + // Flags for add command + blocksAddCmd.Flags().StringVarP(&blockType, "type", "t", "paragraph", "Block type to add") +} diff --git a/utils/block.go b/utils/block.go index a5af46b..8195d8f 100644 --- a/utils/block.go +++ b/utils/block.go @@ -10,6 +10,7 @@ import ( "fmt" "io/ioutil" "net/http" + "sort" "time" ) @@ -17,12 +18,58 @@ var baseURL = "https://api.notion.com/v1" var blocks []Block +// BlockTypeInfo contains display information for a block type +type BlockTypeInfo struct { + Icon string + Color string +} + +// SupportedBlockTypes defines all supported block types with their display info +var SupportedBlockTypes = map[string]BlockTypeInfo{ + "paragraph": {Icon: "¶", Color: "white"}, + "heading_1": {Icon: "H1", Color: "cyan"}, + "heading_2": {Icon: "H2", Color: "cyan"}, + "heading_3": {Icon: "H3", Color: "cyan"}, + "bulleted_list_item": {Icon: "•", Color: "white"}, + "numbered_list_item": {Icon: "#", Color: "white"}, + "to_do": {Icon: "☐", Color: "green"}, + "toggle": {Icon: "▸", Color: "magenta"}, + "quote": {Icon: "❝", Color: "yellow"}, + "callout": {Icon: "💡", Color: "yellow"}, + "divider": {Icon: "—", Color: "white"}, + "code": {Icon: "<>", Color: "blue"}, +} + +// GetSupportedBlockTypeNames returns a sorted list of supported block type names +func GetSupportedBlockTypeNames() []string { + names := make([]string, 0, len(SupportedBlockTypes)) + for name := range SupportedBlockTypes { + names = append(names, name) + } + sort.Strings(names) + return names +} + +// IsValidBlockType checks if a block type is supported +func IsValidBlockType(blockType string) bool { + _, ok := SupportedBlockTypes[blockType] + return ok +} + type ToDo struct { Checked bool `json:"checked"` Color string `json:"color"` RichText []RichText `json:"rich_text"` } +// RichTextBlock represents a block with rich_text content +type RichTextBlock struct { + RichText []RichText `json:"rich_text"` + Color string `json:"color,omitempty"` + Language string `json:"language,omitempty"` // for code blocks + Checked bool `json:"checked,omitempty"` // for to_do blocks +} + type RichText struct { Annotations Annotation `json:"annotations"` Href interface{} `json:"href"` @@ -46,13 +93,24 @@ type Text struct { } type Block struct { - Object string `json:"object"` - ID string `json:"id"` - CreatedTime string `json:"created_time"` - LastEditedTime string `json:"last_edited_time"` - Type string `json:"type"` - HasChildren bool `json:"has_children"` - ToDo *ToDo `json:"to_do,omitempty"` + Object string `json:"object"` + ID string `json:"id"` + CreatedTime string `json:"created_time"` + LastEditedTime string `json:"last_edited_time"` + Type string `json:"type"` + HasChildren bool `json:"has_children"` + ToDo *ToDo `json:"to_do,omitempty"` + Paragraph *RichTextBlock `json:"paragraph,omitempty"` + Heading1 *RichTextBlock `json:"heading_1,omitempty"` + Heading2 *RichTextBlock `json:"heading_2,omitempty"` + Heading3 *RichTextBlock `json:"heading_3,omitempty"` + BulletedListItem *RichTextBlock `json:"bulleted_list_item,omitempty"` + NumberedListItem *RichTextBlock `json:"numbered_list_item,omitempty"` + Toggle *RichTextBlock `json:"toggle,omitempty"` + Quote *RichTextBlock `json:"quote,omitempty"` + Callout *RichTextBlock `json:"callout,omitempty"` + Code *RichTextBlock `json:"code,omitempty"` + Divider *struct{} `json:"divider,omitempty"` } type BlockList struct { @@ -312,3 +370,257 @@ func DeleteToDoBlock(notionAPIKey, pageID string, order int) error { return nil } + +// GetAllBlocks retrieves all blocks under a page, optionally filtered by type +func GetAllBlocks(notionAPIKey, pageID string, filterType string) ([]Block, error) { + client := &http.Client{} + req, err := http.NewRequest("GET", baseURL+"/blocks/"+pageID+"/children", nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + req.Header.Add("accept", "application/json") + req.Header.Add("Notion-Version", "2022-06-28") + req.Header.Set("Authorization", "Bearer "+notionAPIKey) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var blockList BlockList + err = json.NewDecoder(resp.Body).Decode(&blockList) + if err != nil { + return nil, err + } + + var result []Block + for _, block := range blockList.Results { + if block.Object == "block" { + // If no filter or matches filter, include block + if filterType == "" || block.Type == filterType { + result = append(result, block) + } + } + } + return result, nil +} + +// GetBlockContent extracts text content from any block type +func GetBlockContent(block Block) string { + switch block.Type { + case "divider": + return "───────────" + case "to_do": + if block.ToDo != nil && len(block.ToDo.RichText) > 0 { + return block.ToDo.RichText[0].PlainText + } + case "paragraph": + if block.Paragraph != nil && len(block.Paragraph.RichText) > 0 { + return block.Paragraph.RichText[0].PlainText + } + case "heading_1": + if block.Heading1 != nil && len(block.Heading1.RichText) > 0 { + return block.Heading1.RichText[0].PlainText + } + case "heading_2": + if block.Heading2 != nil && len(block.Heading2.RichText) > 0 { + return block.Heading2.RichText[0].PlainText + } + case "heading_3": + if block.Heading3 != nil && len(block.Heading3.RichText) > 0 { + return block.Heading3.RichText[0].PlainText + } + case "bulleted_list_item": + if block.BulletedListItem != nil && len(block.BulletedListItem.RichText) > 0 { + return block.BulletedListItem.RichText[0].PlainText + } + case "numbered_list_item": + if block.NumberedListItem != nil && len(block.NumberedListItem.RichText) > 0 { + return block.NumberedListItem.RichText[0].PlainText + } + case "toggle": + if block.Toggle != nil && len(block.Toggle.RichText) > 0 { + return block.Toggle.RichText[0].PlainText + } + case "quote": + if block.Quote != nil && len(block.Quote.RichText) > 0 { + return block.Quote.RichText[0].PlainText + } + case "callout": + if block.Callout != nil && len(block.Callout.RichText) > 0 { + return block.Callout.RichText[0].PlainText + } + case "code": + if block.Code != nil && len(block.Code.RichText) > 0 { + return block.Code.RichText[0].PlainText + } + } + return "(empty)" +} + +// GetBlockIcon returns the display icon for a block +func GetBlockIcon(block Block) string { + // Special case for to_do: show checked/unchecked + if block.Type == "to_do" && block.ToDo != nil { + if block.ToDo.Checked { + return "☑" + } + return "☐" + } + + if info, ok := SupportedBlockTypes[block.Type]; ok { + return info.Icon + } + return "?" +} + +// FormatAllBlocks returns formatted strings for all blocks +func FormatAllBlocks(notionAPIKey, pageID string, localTimezone *time.Location, filterType string) ([]string, map[string]int, error) { + blocks, err := GetAllBlocks(notionAPIKey, pageID, filterType) + if err != nil { + return nil, nil, err + } + + var formatted []string + typeCounts := make(map[string]int) + + for index, block := range blocks { + lastEditedTime, err := time.Parse(time.RFC3339, block.LastEditedTime) + if err != nil { + return nil, nil, err + } + truncatedTime := lastEditedTime.In(localTimezone).Truncate(time.Minute) + + icon := GetBlockIcon(block) + content := GetBlockContent(block) + + // Truncate long content + if len(content) > 50 { + content = content[:47] + "..." + } + + element := fmt.Sprintf("%4d %s [%-20s] %s (%s)", + index+1, + icon, + block.Type, + content, + truncatedTime.Format("2006-01-02 15:04")) + formatted = append(formatted, element) + typeCounts[block.Type]++ + } + + return formatted, typeCounts, nil +} + +// AddBlock adds a new block of any supported type +func AddBlock(notionAPIKey, pageID, blockType, text string) error { + if !IsValidBlockType(blockType) { + return fmt.Errorf("unsupported block type: %s", blockType) + } + + client := &http.Client{} + + var blockContent map[string]interface{} + + if blockType == "divider" { + blockContent = map[string]interface{}{ + "object": "block", + "type": "divider", + "divider": map[string]interface{}{}, + } + } else { + // Most blocks use rich_text + richText := []map[string]interface{}{ + { + "type": "text", + "text": map[string]interface{}{ + "content": text, + }, + }, + } + + innerContent := map[string]interface{}{ + "rich_text": richText, + } + + // Add language for code blocks + if blockType == "code" { + innerContent["language"] = "plain text" + } + + blockContent = map[string]interface{}{ + "object": "block", + "type": blockType, + blockType: innerContent, + } + } + + reqBody, err := json.Marshal(map[string]interface{}{ + "children": []map[string]interface{}{blockContent}, + }) + if err != nil { + return fmt.Errorf("error marshalling request body: %v", err) + } + + req, err := http.NewRequest("PATCH", baseURL+"/blocks/"+pageID+"/children", bytes.NewBuffer(reqBody)) + if err != nil { + return fmt.Errorf("error creating request: %v", err) + } + + req.Header.Add("Content-Type", "application/json; charset=utf-8") + req.Header.Add("Notion-Version", "2022-06-28") + req.Header.Set("Authorization", "Bearer "+notionAPIKey) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("unexpected status code: %d, message: %s", resp.StatusCode, string(bodyBytes)) + } + + return nil +} + +// DeleteBlock deletes any block by its index (1-based) +func DeleteBlock(notionAPIKey, pageID string, order int) error { + // Get all blocks to find the one at the given index + blocks, err := GetAllBlocks(notionAPIKey, pageID, "") + if err != nil { + return err + } + + if order < 1 || order > len(blocks) { + return fmt.Errorf("block number %d out of range (1-%d)", order, len(blocks)) + } + + blockID := blocks[order-1].ID + + client := &http.Client{} + req, err := http.NewRequest("DELETE", baseURL+"/blocks/"+blockID, nil) + if err != nil { + return fmt.Errorf("error creating request: %v", err) + } + + req.Header.Add("Notion-Version", "2022-06-28") + req.Header.Add("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", "Bearer "+notionAPIKey) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("unexpected status code: %d, message: %s", resp.StatusCode, string(bodyBytes)) + } + + return nil +} diff --git a/utils/block_test.go b/utils/block_test.go index e2087b0..13e0c60 100644 --- a/utils/block_test.go +++ b/utils/block_test.go @@ -153,3 +153,147 @@ func TestDeleteToDoBlock(t *testing.T) { } } + +// Tests for block types functionality + +func TestIsValidBlockType(t *testing.T) { + validTypes := []string{ + "paragraph", "heading_1", "heading_2", "heading_3", + "bulleted_list_item", "numbered_list_item", "to_do", + "toggle", "quote", "callout", "divider", "code", + } + + for _, bt := range validTypes { + if !IsValidBlockType(bt) { + t.Errorf("Expected '%s' to be a valid block type", bt) + } + } + + invalidTypes := []string{"invalid", "unknown", "image", ""} + for _, bt := range invalidTypes { + if IsValidBlockType(bt) { + t.Errorf("Expected '%s' to be an invalid block type", bt) + } + } +} + +func TestGetSupportedBlockTypeNames(t *testing.T) { + names := GetSupportedBlockTypeNames() + + if len(names) != 12 { + t.Errorf("Expected 12 block types, got %d", len(names)) + } + + // Check that list is sorted + for i := 1; i < len(names); i++ { + if names[i] < names[i-1] { + t.Errorf("Block type names not sorted: %s < %s", names[i], names[i-1]) + } + } +} + +func TestGetBlockContent(t *testing.T) { + tests := []struct { + name string + block Block + expected string + }{ + { + name: "to_do block", + block: Block{ + Type: "to_do", + ToDo: &ToDo{RichText: []RichText{{PlainText: "Buy milk"}}}, + }, + expected: "Buy milk", + }, + { + name: "paragraph block", + block: Block{ + Type: "paragraph", + Paragraph: &RichTextBlock{RichText: []RichText{{PlainText: "Hello world"}}}, + }, + expected: "Hello world", + }, + { + name: "heading_1 block", + block: Block{ + Type: "heading_1", + Heading1: &RichTextBlock{RichText: []RichText{{PlainText: "Title"}}}, + }, + expected: "Title", + }, + { + name: "divider block", + block: Block{ + Type: "divider", + Divider: &struct{}{}, + }, + expected: "───────────", + }, + { + name: "empty paragraph", + block: Block{ + Type: "paragraph", + Paragraph: &RichTextBlock{RichText: []RichText{}}, + }, + expected: "(empty)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetBlockContent(tt.block) + if result != tt.expected { + t.Errorf("GetBlockContent() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestGetBlockIcon(t *testing.T) { + tests := []struct { + name string + block Block + expected string + }{ + { + name: "unchecked to_do", + block: Block{Type: "to_do", ToDo: &ToDo{Checked: false}}, + expected: "☐", + }, + { + name: "checked to_do", + block: Block{Type: "to_do", ToDo: &ToDo{Checked: true}}, + expected: "☑", + }, + { + name: "paragraph", + block: Block{Type: "paragraph"}, + expected: "¶", + }, + { + name: "heading_1", + block: Block{Type: "heading_1"}, + expected: "H1", + }, + { + name: "bulleted_list_item", + block: Block{Type: "bulleted_list_item"}, + expected: "•", + }, + { + name: "unknown type", + block: Block{Type: "unknown"}, + expected: "?", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetBlockIcon(tt.block) + if result != tt.expected { + t.Errorf("GetBlockIcon() = %q, want %q", result, tt.expected) + } + }) + } +} From 22a8bbe1b3a7eb23bbae586491f4acd279b5d2e4 Mon Sep 17 00:00:00 2001 From: Kris Hansen Date: Wed, 11 Mar 2026 18:27:29 -0600 Subject: [PATCH 2/3] ci: Add GitHub Actions test workflow - Run tests on push to main and PRs - Build, test, and vet Go code --- .github/workflows/test.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0b05b5c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... + + - name: Vet + run: go vet ./... From d1622583d17ea77856ed35fda50bca9540705c52 Mon Sep 17 00:00:00 2001 From: Kris Hansen Date: Wed, 11 Mar 2026 19:14:30 -0600 Subject: [PATCH 3/3] fix: Add pagination support to GetAllBlocks Handle Notion API pagination for pages with 100+ blocks. Uses start_cursor and has_more to fetch all pages. Addresses PR review feedback from Preston AI. --- utils/block.go | 63 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/utils/block.go b/utils/block.go index 8195d8f..c53f306 100644 --- a/utils/block.go +++ b/utils/block.go @@ -372,38 +372,55 @@ func DeleteToDoBlock(notionAPIKey, pageID string, order int) error { } // GetAllBlocks retrieves all blocks under a page, optionally filtered by type +// Handles pagination for pages with more than 100 blocks func GetAllBlocks(notionAPIKey, pageID string, filterType string) ([]Block, error) { client := &http.Client{} - req, err := http.NewRequest("GET", baseURL+"/blocks/"+pageID+"/children", nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %v", err) - } + var result []Block + var cursor string - req.Header.Add("accept", "application/json") - req.Header.Add("Notion-Version", "2022-06-28") - req.Header.Set("Authorization", "Bearer "+notionAPIKey) + for { + url := baseURL + "/blocks/" + pageID + "/children" + if cursor != "" { + url += "?start_cursor=" + cursor + } - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } - var blockList BlockList - err = json.NewDecoder(resp.Body).Decode(&blockList) - if err != nil { - return nil, err - } + req.Header.Add("accept", "application/json") + req.Header.Add("Notion-Version", "2022-06-28") + req.Header.Set("Authorization", "Bearer "+notionAPIKey) - var result []Block - for _, block := range blockList.Results { - if block.Object == "block" { - // If no filter or matches filter, include block - if filterType == "" || block.Type == filterType { - result = append(result, block) + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + var blockList BlockList + err = json.NewDecoder(resp.Body).Decode(&blockList) + resp.Body.Close() + if err != nil { + return nil, err + } + + for _, block := range blockList.Results { + if block.Object == "block" { + // If no filter or matches filter, include block + if filterType == "" || block.Type == filterType { + result = append(result, block) + } } } + + // Check if there are more pages + if !blockList.HasMore || blockList.NextCursor == "" { + break + } + cursor = blockList.NextCursor } + return result, nil }