【Notion/Go】Go で Notion API を試してみる

9 min read読了の目安(約8100字

2021年5月14日に Notion API のパブリックベータ版がリリースされました。
WebアプリケーションやCLI等と Notion を連携させてみたいなーなんて思ったりしています。
とりあえずどんなことが出来るのか試したかったので、Go で Notion API を使ったプログラムを書いてみました。
既に GitHub にライブラリがいくつか公開されているのですが、今回は標準ライブラリのみを使っていきます。

環境

  • go1.16.5
  • macOS Catalina 10.17.7

Notion側の設定

公式ドキュメントを参考にNotion側の準備をしていきます。

https://developers.notion.com/docs

integration作成

まずはintegrationなるものを作成し、ワークスペースと紐付けます。

ドキュメントには、

Integrations built with the API follow a similar permission system to the sharing permissions for users. There's an important difference: integrations don't have access to any pages (or databases) in the workspace at first. A user must share specific pages with an integration in order for those pages to be accessed using the API. This helps keep you and your team's information in Notion secure.

とあり、作成したページをintegrationにshareすることで、そのページのAPI操作が可能になるようです。

integrationはWeb版での作成となるので、https://www.notion.so/my-integrations にアクセスします。
注意点としてintegration作成時は Notion にAdminでログインしなければいけません。
Admin権限がない場合はintegrationに紐付けられるのは個人ワークスペースのみとなります。
ログインしてアクセスができたら、画面の New integration を押下します。
続いてintegration名、logo、紐付けるワークスペースを設定し Submit を押下することで作成は完了です。
遷移後の画面のSecretsは、API使用時にトークンとして必要になります。
また、Integration typeを選択することでintegrationの公開、非公開の設定ができます。

ページのshare

API経由で操作したいページをintegrartionにshareします。
ページの右上のshareを押下すると、モーダルが表示されintegrationが選択できます。
先ほど作成したintegrationを選択し、inviteを押下します。
またワークスペースのメンバーであれば、adminユーザ以外のユーザでもこの操作は可能です。

今回はページ名のみ付けた空っぽのページをintegrationにshareしました。

APIの使用

ドキュメントを参考に進めていきます。

https://developers.notion.com/reference/intro

ページの取得

integrationにshareしたページを取得します。
ページのID、API KEYが揃っていれば取得可能です。
なおページのIDは、ページにアクセスする際のURLの末尾のものにあたり、API使用時には-を入れてUUIDの形式で扱う必要があります。

const ApiKey = "APIのKey"

func main() {
	client := &http.Client{}
	req, err := http.NewRequest("GET", "https://api.notion.com/v1/pages/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Add("Authorization", "Bearer" + ApiKey)
	req.Header.Add("Notion-Version", "2021-05-13")
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	var out bytes.Buffer
	json.Indent(&out, b, "", "  ")
	out.WriteTo(os.Stdout)
}
$ go run main.go
{
  "object": "page",
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "created_time": "2021-06-05T06:13:00.000Z",
  "last_edited_time": "2021-06-06T01:39:00.000Z",
  "parent": {
    "type": "workspace",
    "workspace": true
  },
  "archived": false,
  "properties": {
    "title": {
      "id": "title",
      "type": "title",
      "title": [
        {
          "type": "text",
          "text": {
            "content": "Sample",
            "link": null
          },
          "annotations": {
            "bold": false,
            "italic": false,
            "strikethrough": false,
            "underline": false,
            "code": false,
            "color": "default"
          },
          "plain_text": "Sample",
          "href": null
        }
      ]
    }
  }
}

ページの作成

がんがんページを作成していこうと思いましたが、作成時にparent(一つの上の階層のオブジェクト)を指定する必要がある為、API経由だとページをワークスペース直下には作成できないと気付きました。
つまりintegrationにshareしたページ、そのページより下の階層ものしか操作できないということです。
そらそうか、という感じです。

ページはPage objectによって構成され、Page objectはページに関する情報(タイトルなど)としてpropertiesを持っています。
また、ページ作成時にchildrenを定義してボディに含めることでページのコンテンツを作成できます。
childrenに見出しと段落を定義してみます。
parentにはintegrationにshareしたページを指定します。

input.json
{
  "parent": {
    "page_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  "properties": {
    "title": {
      "title": [
      	{
      	  "text": {
      	    "content": "Title"
      	  }
      	}
      ]
    }
  },
  "children": [
    {
      "object": "block",
      "type": "heading_2",
      "heading_2": {
        "text": [
          {
            "type": "text",
            "text": {
              "content": "見出しです"
            }
          }
        ]
      }
    },
    {
      "object": "block",
      "type": "paragraph",
      "paragraph": {
        "text": [
          {
            "type": "text",
            "text": {
              "content": "段落です"
            }
          }
        ]
      }
    }
  ]
}
const ApiKey = "APIのKey"

func main() {
	client := &http.Client{}
	jsonBody, err := ioutil.ReadFile("input.json")
	if err != nil {
		log.Fatal(err)
	}
	body := strings.NewReader(string(jsonBody))
	req, err := http.NewRequest("POST", "https://api.notion.com/v1/pages", body)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Add("Authorization", "Bearer " + ApiKey)
	req.Header.Add("Notion-Version", "2021-05-13")
	req.Header.Add("Content-Type", "application/json")
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	var out bytes.Buffer
	json.Indent(&out, b, "", "  ")
	out.WriteTo(os.Stdout)
}
$ go run main.go
{
  "object": "page",
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "created_time": "2021-06-06T01:50:11.022Z",
  "last_edited_time": "2021-06-06T01:50:11.023Z",
  "parent": {
    "type": "page_id",
    "page_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  "archived": false,
  "properties": {
    "title": {
      "id": "title",
      "type": "title",
      "title": [
        {
          "type": "text",
          "text": {
            "content": "Title",
            "link": null
          },
          "annotations": {
            "bold": false,
            "italic": false,
            "strikethrough": false,
            "underline": false,
            "code": false,
            "color": "default"
          },
          "plain_text": "Title",
          "href": null
        }
      ]
    }
  }
}


想定通り作成されています。

ページの更新

ページはいくつかのプロパティを持っており、更新が可能です。
タイトルを変更し、チェックボックスを追加してみます。
パスパラメータのページのIDには先ほど作成したページのIDを入れます。

input.json
{
  "properties": {
    "title": {
      "title": [
        {
          "text": {
            "content": "Updated Title"
          }
        }
      ]
    }
  }
}
const ApiKey = "APIのKey"

func main() {
	client := &http.Client{}
	jsonBody, err := ioutil.ReadFile("input.json")
	if err != nil {
		log.Fatal(err)
	}
	body := strings.NewReader(string(jsonBody))
	req, err := http.NewRequest("PATCH", "https://api.notion.com/v1/pages/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", body)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Add("Authorization", "Bearer" + ApiKey)
	req.Header.Add("Notion-Version", "2021-05-13")
	req.Header.Add("Content-Type", "application/json")
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	var out bytes.Buffer
	json.Indent(&out, b, "", "  ")
	out.WriteTo(os.Stdout)
}
$ go run main.go
{
  "object": "page",
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "created_time": "2021-06-06T01:50:11.022Z",
  "last_edited_time": "2021-06-06T01:54:19.397Z",
  "parent": {
    "type": "page_id",
    "page_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  "archived": false,
  "properties": {
    "title": {
      "id": "title",
      "type": "title",
      "title": [
        {
          "type": "text",
          "text": {
            "content": "Updated Title",
            "link": null
          },
          "annotations": {
            "bold": false,
            "italic": false,
            "strikethrough": false,
            "underline": false,
            "code": false,
            "color": "default"
          },
          "plain_text": "Updated Title",
          "href": null
        }
      ]
    }
  }
}

想定通り変更されています。

さいごに

今回はひとまずページの操作を試してみました。
他にもデータベースやユーザの操作などのAPIも公開されています。
これからどんどんAPIが追加されていくのが楽しみです。

また、 Go でHTTPクライアントを実装するのが楽しかったので、いろんなクライアント実装してみたいななんて思いました。