notion-api-源码分析

694 阅读11分钟

【源码分析】

Notion api doc

  • 主要是api的doc的文档的信息
  • README.md
- tutorial: https://blog.kowalczyk.info/article/c9df78cbeaae4e0cb2848c9964bcfc94/using-notion-api-go-client.html
- API docs: https://pkg.go.dev/github.com/kjk/notionapi

You can learn how [I reverse-engineered the Notion API](https://blog.kowalczyk.info/article/88aee8f43620471aa9dbcad28368174c/how-i-reverse-engineered-notion-api.html) in order to write this library.

# Real-life usage

I use this API to publish my [blog](https://blog.kowalczyk.info/) and series of [programming books](https://www.programming-books.io/) from content stored in Notion.

doPost-请求的对象的处理操作

  • 能力的操作和结构的操作,请求的内部的处理的操作
  1. 支持3次重试的处理操作
  • client.go
func (c *Client) doPost(uri string, body []byte) ([]byte, error) {
	if c.httpPostOverride != nil {
		return c.httpPostOverride(uri, body)
	}
	return c.doPostInternal(uri, body)
}

func (c *Client) doPostInternal(uri string, body []byte) ([]byte, error) {
	c.rateLimitRequest()

	// try to back-off exponentially
	// note: backing off doesn't seem to work i.e. I get 429 from subsequent requests as well
	nRepeats := 0
	timeouts := []time.Duration{time.Second * 3, time.Second * 5, time.Second * 10}
repeatRequest:
	br := bytes.NewBuffer(body)
	req, err := http.NewRequest("POST", uri, br)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", userAgent)
	req.Header.Set("Accept-Language", acceptLang)
	if c.AuthToken != "" {
		req.Header.Set("cookie", fmt.Sprintf("token_v2=%v", c.AuthToken))
	}
	var rsp *http.Response

	httpClient := c.getHTTPClient()
	rsp, err = httpClient.Do(req)

	if err != nil {
		c.logf("httpClient.Do() failed with %s\n", err)
		return nil, err
	}

	if rsp.StatusCode == http.StatusTooManyRequests {
		if nRepeats < 3 {
			closeNoError(rsp.Body)
			c.logf("retrying '%s' because httpClient.Do() returned %d (%s)\n", uri, rsp.StatusCode, rsp.Status)
			time.Sleep(timeouts[nRepeats])
			nRepeats++
			goto repeatRequest
		}
	}

	defer closeNoError(rsp.Body)

	if rsp.StatusCode != 200 {
		d, _ := ioutil.ReadAll(rsp.Body)
		c.logf("Error: status code %s\nBody:\n%s\n", rsp.Status, PrettyPrintJS(d))
		return nil, fmt.Errorf("http.Post('%s') returned non-200 status code of %d", uri, rsp.StatusCode)
	}
	d, err := ioutil.ReadAll(rsp.Body)
	if err != nil {
		c.logf("Error: ioutil.ReadAll() failed with %s\n", err)
		return nil, err
	}
	return d, nil
}

// 调用Notion API
func (c *Client) doNotionAPI(apiURL string, requestData interface{}, result interface{}) (map[string]interface{}, error) {
	var body []byte
	var err error
	if requestData != nil {
		body, err = jsonit.MarshalIndent(requestData, "", "  ")
		if err != nil {
			return nil, err
		}
	}
	uri := notionHost + apiURL
	c.logf("POST %s\n", uri)
	if len(body) > 0 {
		logJSON(c, body)
	}

	d, err := c.doPost(uri, body)
	if err != nil {
		return nil, err
	}
	logJSON(c, d)

	err = jsonit.Unmarshal(d, result)
	if err != nil {
		c.logf("Error: json.Unmarshal() failed with %s\n. Body:\n%s\n", err, string(d))
		return nil, err
	}
	var m map[string]interface{}
	err = jsonit.Unmarshal(d, &m)
	if err != nil {
		return nil, err
	}
	return m, nil
}

Block-0.1-数据结构

  • 这个是核心的结构,主要是为了构建这个block的结构,收费也是根据这个来的。
  • block.go
// Block describes a block
type Block struct {
	// values that come from JSON
	// a unique ID of the block
	ID string `json:"id"`
	// if false, the page is deleted
	Alive bool `json:"alive"`
	// List of block ids for that make up content of this block
	// Use Content to get corresponding block (they are in the same order)
	ContentIDs   []string `json:"content,omitempty"`
	CopiedFrom   string   `json:"copied_from,omitempty"`
	CollectionID string   `json:"collection_id,omitempty"` // for BlockCollectionView
	// ID of the user who created this block
	CreatedBy   string `json:"created_by"`
	CreatedTime int64  `json:"created_time"`

	CreatedByTable string `json:"created_by_table"` // e.g. "notion_user"
	CreatedByID    string `json:"created_by_id"`    // e.g. "bb760e2d-d679-4b64-b2a9-03005b21870a",
	// ID of the user who last edited this block
	LastEditedBy      string `json:"last_edited_by"`
	LastEditedTime    int64  `json:"last_edited_time"`
	LastEditedByTable string `json:"last_edited_by_table"` // e.g. "notion_user"
	LastEditedByID    string `json:"last_edited_by_id"`    // e.g. "bb760e2d-d679-4b64-b2a9-03005b21870a"

	// List of block ids with discussion content
	DiscussionIDs []string `json:"discussion,omitempty"`
	// those ids seem to map to storage in s3
	// https://s3-us-west-2.amazonaws.com/secure.notion-static.com/${id}/${name}
	FileIDs []string `json:"file_ids,omitempty"`

	// TODO: don't know what this means
	IgnoreBlockCount bool `json:"ignore_block_count,omitempty"`

	// ID of parent Block
	ParentID    string `json:"parent_id"`
	ParentTable string `json:"parent_table"`
	// not always available
	Permissions *[]Permission          `json:"permissions,omitempty"`
	Properties  map[string]interface{} `json:"properties,omitempty"`
	SpaceID     string                 `json:"space_id"`
	// type of the block e.g. TypeText, TypePage etc.
	Type string `json:"type"`
	// blocks are versioned
	Version int64 `json:"version"`
	// for BlockCollectionView
	ViewIDs []string `json:"view_ids,omitempty"`

	// Parent of this block
	Parent *Block `json:"-"`

	// maps ContentIDs array to Block type
	Content []*Block `json:"-"`
	// this is for some types like TypePage, TypeText, TypeHeader etc.
	InlineContent []*TextSpan `json:"-"`

	// for BlockPage
	Title string `json:"-"`

	// For BlockTodo, a checked state
	IsChecked bool `json:"-"`

	// for BlockBookmark
	Description string `json:"-"`
	Link        string `json:"-"`

	// for BlockBookmark it's the url of the page
	// for BlockGist it's the url for the gist
	// for BlockImage it's url of the image. Sometimes you need to use DownloadFile()
	//   to get this image
	// for BlockFile it's url of the file
	// for BlockEmbed it's url of the embed
	Source string `json:"-"`

	// for BlockFile
	FileSize string `json:"-"`

	// for BlockCode
	Code         string `json:"-"`
	CodeLanguage string `json:"-"`

	// for BlockCollectionView. There can be multiple views
	// those correspond to ViewIDs
	TableViews []*TableView `json:"-"`

	Page *Page `json:"-"`

	// RawJSON represents Block as
	RawJSON map[string]interface{} `json:"-"`

	notionID       *NotionID
	parentNotionID *NotionID
	isResolved     bool
}

block-0.2-解析标题和属性的信息

  • note
  • collection.go

func parseTitle(block *Block) error {
	// has already been parsed
	if block.InlineContent != nil {
		return nil
	}
	props := block.Properties
	title, ok := props["title"]
	if !ok {
		return nil
	}
	var err error

	block.InlineContent, err = ParseTextSpans(title)
	if err != nil {
		return err
	}
	switch block.Type {
	case BlockPage, BlockFile, BlockBookmark:
		block.Title, err = getInlineText(title)
	case BlockCode:
		block.Code, err = getInlineText(title)
	default:
	}
	return err
}

func parseProperties(block *Block) error {
	err := parseTitle(block)
	if err != nil {
		return err
	}

	props := block.Properties

	if BlockTodo == block.Type {
		if checked, ok := props["checked"]; ok {
			s, _ := getFirstInlineBlock(checked)
			// fmt.Printf("checked: '%s'\n", s)
			block.IsChecked = strings.EqualFold(s, "Yes")
		}
	}

	// for BlockBookmark
	getProp(block, "description", &block.Description)
	// for BlockBookmark
	getProp(block, "link", &block.Link)

	// for BlockBookmark, BlockImage, BlockGist, BlockFile, BlockEmbed
	// don't over-write if was already set from "source" json field
	if block.Source == "" {
		getProp(block, "source", &block.Source)
	}

	// for BlockCode
	getProp(block, "language", &block.CodeLanguage)

	// for BlockFile
	if block.Type == BlockFile {
		getProp(block, "size", &block.FileSize)
	}

	return nil
}


func parseTextSpanAttributes(b *TextSpan, a []interface{}) error {
	for _, rawAttr := range a {
		attrList, ok := rawAttr.([]interface{})
		if !ok {
			return fmt.Errorf("rawAttr is not []interface{} but %T of value %#v", rawAttr, rawAttr)
		}
		err := parseTextSpanAttribute(b, attrList)
		if err != nil {
			return err
		}
	}
	return nil
}

func parseTextSpan(a []interface{}) (*TextSpan, error) {
	if len(a) == 0 {
		return nil, fmt.Errorf("a is empty")
	}

	if len(a) == 1 {
		s, ok := a[0].(string)
		if !ok {
			return nil, fmt.Errorf("a is of length 1 but not string. a[0] el type: %T, el value: '%#v'", a[0], a[0])
		}
		return &TextSpan{
			Text: s,
		}, nil
	}
	if len(a) != 2 {
		return nil, fmt.Errorf("a is of length != 2. a value: '%#v'", a)
	}

	s, ok := a[0].(string)
	if !ok {
		return nil, fmt.Errorf("a[0] is not string. a[0] type: %T, value: '%#v'", a[0], a[0])
	}
	res := &TextSpan{
		Text: s,
	}
	a, ok = a[1].([]interface{})
	if !ok {
		return nil, fmt.Errorf("a[1] is not []interface{}. a[1] type: %T, value: '%#v'", a[1], a[1])
	}
	err := parseTextSpanAttributes(res, a)
	if err != nil {
		return nil, err
	}
	return res, nil
}

// ParseTextSpans parses content from JSON into an easier to use form
func ParseTextSpans(raw interface{}) ([]*TextSpan, error) {
	if raw == nil {
		return nil, nil
	}
	var res []*TextSpan
	a, ok := raw.([]interface{})
	if !ok {
		return nil, fmt.Errorf("raw is not of []interface{}. raw type: %T, value: '%#v'", raw, raw)
	}
	if len(a) == 0 {
		return nil, fmt.Errorf("raw is empty")
	}
	for _, v := range a {
		a2, ok := v.([]interface{})
		if !ok {
			return nil, fmt.Errorf("v is not []interface{}. v type: %T, value: '%#v'", v, v)
		}
		span, err := parseTextSpan(a2)
		if err != nil {
			return nil, err
		}
		res = append(res, span)
	}
	return res, nil
}

Json的工具的格式的处理

  • note
  • json.go
func PrettyPrintJSJsonit(js []byte) []byte {
	if !jsonit.Valid(js) {
		return js
	}
	return pretty.PrettyOptions(js, &prettyOpts)
}

// pretty-print if valid JSON. If not, return unchanged
// about 4x faster than naive version using json.Unmarshal() + json.Marshal()
func PrettyPrintJSStd(js []byte) []byte {
	var m map[string]interface{}
	err := json.Unmarshal(js, &m)
	if err != nil {
		return js
	}
	d, err := json.MarshalIndent(m, "", "  ")
	if err != nil {
		return js
	}
	return d
}

func jsonUnmarshalFromMap(m map[string]interface{}, v interface{}) error {
	d, err := jsonit.Marshal(m)
	if err != nil {
		return err
	}
	return json.Unmarshal(d, v)
}

func jsonGetMap(m map[string]interface{}, key string) map[string]interface{} {
	if v, ok := m[key]; ok {
		if m, ok := v.(map[string]interface{}); ok {
			return m
		}
	}
	return nil
}

Permission-权限的管理的处理

  • 可以获取用户的权限和角色的关系
  • block.go
// Permission represents user permissions o
type Permission struct {
	Type string `json:"type"`

	// common to some permission types
	Role interface{} `json:"role"`

	// if Type == "user_permission"
	UserID *string `json:"user_id,omitempty"`

	AddedTimestamp int64 `json:"added_timestamp"`

	// if Type == "public_permission"
	AllowDuplicate            bool `json:"allow_duplicate"`
	AllowSearchEngineIndexing bool `json:"allow_search_engine_indexing"`
}

tableview-0.1-表格的支持多种视图的切换

  • note
  • collection.go
// TableView represents a view of a table (Notion calls it a Collection View)
// Meant to be a representation that is easier to work with
type TableView struct {
	// original data
	Page           *Page
	CollectionView *CollectionView
	Collection     *Collection

	// easier to work representation we calculate
	Columns []*ColumnInfo
	Rows    []*TableRow
}

page-表格和page是多对多的关系,这样里面是可以相互嵌套的

  • note
  • page.go
// Page describes a single Notion page
type Page struct {
	ID       string
	NotionID *NotionID

	// expose raw records for all data associated with this page
	BlockRecords          []*Record
	UserRecords           []*Record
	CollectionRecords     []*Record
	CollectionViewRecords []*Record
	DiscussionRecords     []*Record
	CommentRecords        []*Record
	SpaceRecords          []*Record

	// for every block of type collection_view and its view_ids
	// we } TableView representing that collection view_id
	TableViews []*TableView

	idToBlock          map[string]*Block
	idToUser           map[string]*User
	idToCollection     map[string]*Collection
	idToCollectionView map[string]*CollectionView
	idToComment        map[string]*Comment
	idToDiscussion     map[string]*Discussion
	idToSpace          map[string]*Space

	blocksToSkip map[string]struct{} // not alive or when server doesn't return "value" for this block id

	client   *Client
	subPages []*NotionID
}

Activity-类似于事件的日志,也就是每一个操作就会记录在activity里面下

  • activity可以为每个操作记录下信息,尤其是对于block记录,方便后面进行收费的处理操作
  • export_page.go
// Edit represents a Notion edit (ie. a change made during an Activity)
type Edit struct {
	SpaceID   string   `json:"space_id"`
	Authors   []Author `json:"authors"`
	Timestamp int64    `json:"timestamp"`
	Type      string   `json:"type"`
	Version   int      `json:"version"`

	CommentData  Comment `json:"comment_data"`
	CommentID    string  `json:"comment_id"`
	DiscussionID string  `json:"discussion_id"`

	BlockID   string `json:"block_id"`
	BlockData struct {
		BlockValue Block `json:"block_value"`
	} `json:"block_data"`
	NavigableBlockID string `json:"navigable_block_id"`

	CollectionID    string `json:"collection_id"`
	CollectionRowID string `json:"collection_row_id"`
}

// Activity represents a Notion activity (ie. event)
type Activity struct {
	Role string `json:"role"`

	ID        string `json:"id"`
	SpaceID   string `json:"space_id"`
	StartTime string `json:"start_time"`
	EndTime   string `json:"end_time"`
	Type      string `json:"type"`
	Version   int    `json:"version"`

	ParentID    string `json:"parent_id"`
	ParentTable string `json:"parent_table"`

	// If the edit was to a block inside a regular page
	NavigableBlockID string `json:"navigable_block_id"`

	// If the edit was to a block inside a collection or collection row
	CollectionID    string `json:"collection_id"`
	CollectionRowID string `json:"collection_row_id"`

	Edits []Edit `json:"edits"`

	Index   int  `json:"index"`
	Invalid bool `json:"invalid"`

	RawJSON map[string]interface{} `json:"-"`
}

// 获取notion的使用记录的情况
func (c *Client) GetActivityLog(spaceID string, startingAfterID string, limit int) (*GetActivityLogResponse, error) {
	req := &getActivityLogRequest{
		SpaceID:         spaceID,
		StartingAfterID: startingAfterID,
		Limit:           limit,
	}
	var rsp GetActivityLogResponse
	var err error
	apiURL := "/api/v3/getActivityLog"
	if rsp.RawJSON, err = c.doNotionAPI(apiURL, req, &rsp); err != nil {
		return nil, err
	}
	if err = ParseRecordMap(rsp.RecordMap); err != nil {
		return nil, err
	}
	if len(rsp.ActivityIDs) > 0 {
		rsp.NextID = rsp.ActivityIDs[len(rsp.ActivityIDs)-1]
	} else {
		rsp.NextID = ""
	}
	return &rsp, nil
}


CollectionView-集合视图的情况

  • 集合的视图的能力的构建操作+获取能力的构建操作
  1. 填充表格的,表格记录,包含了拥有的表格视图,页面快和列的文本块信息
  • collection.go
// CollectionView represents a collection view
type CollectionView struct {
	ID          string       `json:"id"`
	Version     int64        `json:"version"`
	Type        string       `json:"type"` // "table"
	Format      *FormatTable `json:"format"`
	Name        string       `json:"name"`
	ParentID    string       `json:"parent_id"`
	ParentTable string       `json:"parent_table"`
	Query       *Query       `json:"query2"`
	Alive       bool         `json:"alive"`
	PageSort    []string     `json:"page_sort"`
	SpaceID     string       `json:"space_id"`

	// set by us
	RawJSON map[string]interface{} `json:"-"`
}

// FormatTable describes format for BlockTable
type FormatTable struct {
	PageSort        []string         `json:"page_sort"`
	TableWrap       bool             `json:"table_wrap"`
	TableProperties []*TableProperty `json:"table_properties"`
}


type TableRow struct {
	// TableView that owns this row
	TableView *TableView

	// data for row is stored as properties of a page
	Page *Block

	// values extracted from Page for each column
	Columns [][]*TextSpan
}

// TextAttr describes attributes of a span of text
// First element is name of the attribute (e.g. AttrLink)
// The rest are optional information about attribute (e.g.
// for AttrLink it's URL, for AttrUser it's user id etc.)
type TextAttr = []string

// TextSpan describes a text with attributes
type TextSpan struct {
	Text  string     `json:"Text"`
	Attrs []TextAttr `json:"Attrs"`
}

// Collection describes a collection
type Collection struct {
	ID          string                   `json:"id"`
	Version     int                      `json:"version"`
	Name        interface{}              `json:"name"`
	Schema      map[string]*ColumnSchema `json:"schema"`
	Format      *CollectionFormat        `json:"format"`
	ParentID    string                   `json:"parent_id"`
	ParentTable string                   `json:"parent_table"`
	Alive       bool                     `json:"alive"`
	CopiedFrom  string                   `json:"copied_from"`
	Cover       string                   `json:"cover"`
	Description []interface{}            `json:"description"`

	// TODO: are those ever present?
	Type          string   `json:"type"`
	FileIDs       []string `json:"file_ids"`
	Icon          string   `json:"icon"`
	TemplatePages []string `json:"template_pages"`

	// calculated by us
	name    []*TextSpan
	RawJSON map[string]interface{} `json:"-"`
}

space-用于描述workspace的情况,包括创建和构建的情况

  • 把space的填充,把space的体系的构建起来的操作
  • space.go
// Space describes Notion workspace.
type Space struct {
	ID                  string                  `json:"id"`
	Version             float64                 `json:"version"`
	Name                string                  `json:"name"`
	Domain              string                  `json:"domain"`
	Permissions         []*SpacePermissions     `json:"permissions,omitempty"`
	PermissionGroups    []SpacePermissionGroups `json:"permission_groups"`
	Icon                string                  `json:"icon"`
	EmailDomains        []string                `json:"email_domains"`
	BetaEnabled         bool                    `json:"beta_enabled"`
	Pages               []string                `json:"pages,omitempty"`
	DisablePublicAccess bool                    `json:"disable_public_access"`
	DisableGuests       bool                    `json:"disable_guests"`
	DisableMoveToSpace  bool                    `json:"disable_move_to_space"`
	DisableExport       bool                    `json:"disable_export"`
	CreatedBy           string                  `json:"created_by"`
	CreatedTime         int64                   `json:"created_time"`
	LastEditedBy        string                  `json:"last_edited_by"`
	LastEditedTime      int64                   `json:"last_edited_time"`

	RawJSON map[string]interface{} `json:"-"`
}

Query-查询过滤器, 对于查询的能力的吹了

  • 把查询的结果信息并且存储起来,便于使用和操作
  • collection.go

type QuerySort struct {
	ID        string `json:"id"`
	Type      string `json:"type"`
	Property  string `json:"property"`
	Direction string `json:"direction"`
}

type QueryAggregate struct {
	ID              string `json:"id"`
	Type            string `json:"type"`
	Property        string `json:"property"`
	ViewType        string `json:"view_type"`
	AggregationType string `json:"aggregation_type"`
}

type QueryAggregation struct {
	Property   string `json:"property"`
	Aggregator string `json:"aggregator"`
}

type Query struct {
	Sort         []QuerySort            `json:"sort"`
	Aggregate    []QueryAggregate       `json:"aggregate"`
	Aggregations []QueryAggregation     `json:"aggregations"`
	Filter       map[string]interface{} `json:"filter"`
}

block-0.3-对于块的信息的处理和操作

  • 对于block的数据库的处理操作,并且把数据集中处理下
  • page.go

func forEachBlockWithParent(seen map[string]bool, blocks []*Block, parent *Block, cb func(*Block)) {
	for _, block := range blocks {
		id := block.ID
		if seen[id] {
			// crash rather than have infinite recursion
			panic("seen the same page again")
		}
		if parent != nil && (block.Type == BlockPage || block.Type == BlockCollectionViewPage) {
			// skip sub-pages to avoid infnite recursion
			continue
		}
		seen[id] = true
		block.Parent = parent
		cb(block)
		forEachBlockWithParent(seen, block.Content, block, cb)
	}
}

// ForEachBlock traverses the tree of blocks and calls cb on every block
// in depth-first order. To traverse every blocks in a Page, do:
// ForEachBlock([]*notionapi.Block{page.Root}, cb)
func ForEachBlock(blocks []*Block, cb func(*Block)) {
	seen := map[string]bool{}
	forEachBlockWithParent(seen, blocks, nil, cb)
}

// ForEachBlock recursively calls cb for each block in the page
func (p *Page) ForEachBlock(cb func(*Block)) {
	seen := map[string]bool{}
	blocks := []*Block{p.Root()}
	forEachBlockWithParent(seen, blocks, nil, cb)
}

page-GetSubPages-获取子页面的数据信息

  • 对于子页面的信息,并且遍历其特性和能力点
  • page.go
// GetSubPages return list of ids for direct sub-pages of this page
func (p *Page) GetSubPages() []*NotionID {
	if len(p.subPages) > 0 {
		return p.subPages
	}
	root := p.Root()
	panicIf(!isPageBlock(root))
	subPages := map[*NotionID]struct{}{}
	seenBlocks := map[string]struct{}{}
	var blocksToVisit []*NotionID
	for _, id := range root.ContentIDs {
		nid := NewNotionID(id)
		blocksToVisit = append(blocksToVisit, nid)
	}
	for len(blocksToVisit) > 0 {
		nid := blocksToVisit[0]
		id := nid.DashID
		blocksToVisit = blocksToVisit[1:]
		if _, ok := seenBlocks[id]; ok {
			continue
		}
		seenBlocks[id] = struct{}{}
		block := p.BlockByID(nid)
		if p.IsSubPage(block) {
			subPages[nid] = struct{}{}
		}
		// need to recursively scan blocks with children
		for _, id := range block.ContentIDs {
			nid := NewNotionID(id)
			blocksToVisit = append(blocksToVisit, nid)
		}
	}
	res := []*NotionID{}
	for id := range subPages {
		res = append(res, id)
	}
	sort.Slice(res, func(i, j int) bool {
		return res[i].DashID < res[j].DashID
	})
	p.subPages = res
	return res
}

export-把页面导出成html和markdown的格式用于备份的处理操作

  • note
  • export_page.go
// ExportPages exports a page as html or markdown, potentially recursively
func (c *Client) ExportPages(id string, exportType string, recursive bool) ([]byte, error) {
	exportURL, err := c.RequestPageExportURL(id, exportType, recursive)
	if err != nil {
		return nil, err
	}

	dlRsp, err := c.DownloadFile(exportURL, nil)
	if err != nil {
		return nil, err
	}
	return dlRsp.Data, nil
}

// DownloadFile downloads a file stored in Notion referenced
// by a block with a given id and of a given block with a given
// parent table (data present in Block)
func (c *Client) DownloadFile(uri string, block *Block) (*DownloadFileResponse, error) {
	// first try downloading proxied url
	uri2 := maybeProxyImageURL(uri, block)
	res, err := c.DownloadURL(uri2)
	if err != nil && uri2 != uri {
		// otherwise just try your luck with original URL
		res, err = c.DownloadURL(uri)
	}
	if err != nil {
		rsp, err2 := c.GetSignedURLs([]string{uri}, block)
		if err2 != nil {
			return nil, err
		}
		if len(rsp.SignedURLS) == 0 {
			return nil, err
		}
		uri3 := rsp.SignedURLS[0]
		res, err = c.DownloadURL(uri3)
	}
	return res, err
}

record-parseRecord-根据对象获取解析的对象信息

  • 对象信息的解析和能力的构建
  • get_record_values.go
func parseRecord(table string, r *Record) error {
	// it's ok if some records don't return a value
	if len(r.Value) == 0 {
		return nil
	}
	if r.Table == "" {
		r.Table = table
	} else {
		// TODO: probably never happens
		panicIf(r.Table != table)
	}

	// set Block/Space etc. based on TableView type
	var pRawJSON *map[string]interface{}
	var obj interface{}
	switch table {
	case TableActivity:
		r.Activity = &Activity{}
		obj = r.Activity
		pRawJSON = &r.Activity.RawJSON
	case TableBlock:
		r.Block = &Block{}
		obj = r.Block
		pRawJSON = &r.Block.RawJSON
	case TableUser:
		r.User = &User{}
		obj = r.User
		pRawJSON = &r.User.RawJSON
	case TableSpace:
		r.Space = &Space{}
		obj = r.Space
		pRawJSON = &r.Space.RawJSON
	case TableCollection:
		r.Collection = &Collection{}
		obj = r.Collection
		pRawJSON = &r.Collection.RawJSON
	case TableCollectionView:
		r.CollectionView = &CollectionView{}
		obj = r.CollectionView
		pRawJSON = &r.CollectionView.RawJSON
	case TableDiscussion:
		r.Discussion = &Discussion{}
		obj = r.Discussion
		pRawJSON = &r.Discussion.RawJSON
	case TableComment:
		r.Comment = &Comment{}
		obj = r.Comment
		pRawJSON = &r.Comment.RawJSON
	}
	if obj == nil {
		return fmt.Errorf("unsupported table '%s'", r.Table)
	}
	if err := jsonit.Unmarshal(r.Value, pRawJSON); err != nil {
		return err
	}
	id := (*pRawJSON)["id"]
	if id != nil {
		r.ID = id.(string)
	}
	if err := jsonit.Unmarshal(r.Value, &obj); err != nil {
		return err
	}
	return nil
}

SubscriptionData-订阅和付费的信息的处理

  • 把订阅的服务和信息填充起来,便于统计需要付费的情况。
  • get_subscription_data.go
type SubscriptionData struct {
	Type              string                       `json:"type"`
	SpaceUsers        []SubscriptionDataSpaceUsers `json:"spaceUsers"`
	Credits           []SubscriptionDataCredits    `json:"credits"`
	TotalCredit       int                          `json:"totalCredit"`
	AvailableCredit   int                          `json:"availableCredit"`
	CreditEnabled     bool                         `json:"creditEnabled"`
	CustomerID        string                       `json:"customerId"`
	CustomerName      string                       `json:"customerName"`
	VatID             string                       `json:"vatId"`
	IsDelinquent      bool                         `json:"isDelinquent"`
	ProductID         string                       `json:"productId"`
	BillingEmail      string                       `json:"billingEmail"`
	Plan              string                       `json:"plan"`
	PlanAmount        int                          `json:"planAmount"`
	AccountBalance    int                          `json:"accountBalance"`
	MonthlyPlanAmount int                          `json:"monthlyPlanAmount"`
	YearlyPlanAmount  int                          `json:"yearlyPlanAmount"`
	Quantity          int                          `json:"quantity"`
	Billing           string                       `json:"billing"`
	Address           SubscriptionDataAddress      `json:"address"`
	Last4             string                       `json:"last4"`
	Brand             string                       `json:"brand"`
	Interval          string                       `json:"interval"`
	Created           int64                        `json:"created"`
	PeriodEnd         int64                        `json:"periodEnd"`
	NextInvoiceTime   int64                        `json:"nextInvoiceTime"`
	NextInvoiceAmount int                          `json:"nextInvoiceAmount"`
	IsPaid            bool                         `json:"isPaid"`
	Members           []interface{}                `json:"members"`

	RawJSON map[string]interface{} `json:"-"`
}

// GetSubscriptionData executes a raw API call /api/v3/getSubscriptionData
func (c *Client) GetSubscriptionData(spaceID string) (*SubscriptionData, error) {
	req := &struct {
		SpaceID string `json:"spaceId"`
	}{
		SpaceID: spaceID,
	}

	var rsp SubscriptionData
	var err error
	apiURL := "/api/v3/getSubscriptionData"
	rsp.RawJSON, err = c.doNotionAPI(apiURL, req, &rsp)
	if err != nil {
		return nil, err
	}

	return &rsp, nil
}

main-启动http的服务的能力

  • 构建http的服务,然后把服务的能力的构建起来操作
  • main.go
func startHTTPServer(uri string) {
	flgHTTPAddr := "localhost:8503"
	httpSrv := makeHTTPServer()
	httpSrv.Addr = flgHTTPAddr

	logf("Starting on addr: %v\n", flgHTTPAddr)

	chServerClosed := make(chan bool, 1)
	go func() {
		err := httpSrv.ListenAndServe()
		// mute error caused by Shutdown()
		if err == http.ErrServerClosed {
			err = nil
		}
		must(err)
		logf("HTTP server shutdown gracefully\n")
		chServerClosed <- true
	}()

	c := make(chan os.Signal, 2)
	signal.Notify(c, os.Interrupt /* SIGINT */, syscall.SIGTERM)

	openBrowser("http://" + flgHTTPAddr + uri)
	time.Sleep(time.Second * 2)

	sig := <-c
	logf("Got signal %s\n", sig)

	if httpSrv != nil {
		// Shutdown() needs a non-nil context
		_ = httpSrv.Shutdown(context.Background())
		select {
		case <-chServerClosed:
			// do nothing
		case <-time.After(time.Second * 5):
			// timeout
		}
	}

}

sync-对于指定的block进行同步和能力的构建操作

  • 构建的体系和negligib的操作,并且支撑其能力的
  • sync_record_values.go

// SyncRecordValues executes a raw API call /api/v3/syncRecordValues
func (c *Client) SyncRecordValues(records []SyncRecordRequest) (*SyncRecordValuesResponse, error) {
	req := &syncRecordValuesRequest{
		Requests: records,
	}

	var rsp SyncRecordValuesResponse
	var err error
	apiURL := "/api/v3/syncRecordValues"
	if rsp.RawJSON, err = c.doNotionAPI(apiURL, req, &rsp); err != nil {
		return nil, err
	}

	for table, records := range rsp.RecordMap {
		for _, r := range records {
			err = parseRecord(table, r)
			rsp.Results = append(rsp.Results, r)
			if err != nil {
				return nil, err
			}
		}
	}
	return &rsp, nil
}

// SyncBlockRecords executes a raw API call /api/v3/getRecordValues
// to get records for blocks with given ids
func (c *Client) SyncBlockRecords(ids []string) (*SyncRecordValuesResponse, error) {
	records := make([]SyncRecordRequest, len(ids))
	for pos, id := range ids {
		dashID := ToDashID(id)
		if !IsValidDashID(dashID) {
			return nil, fmt.Errorf("'%s' is not a valid notion id", id)
		}
		r := &records[pos]
		r.Version = -1
		r.Pointer.Table = TableBlock
		r.Pointer.ID = dashID
	}
	return c.SyncRecordValues(records)
}