この記事では esa API v1について説明します。
esa API v1は現在public βの状態にあります。
ベータ期間中もできるだけ非互換の変更は避けるつもりですが、APIの仕様を変更することがありますので適宜この記事を確認して下さい。
β期間中にフィードバックを頂いて、esa APIおよびAPIドキュメントをより良いものにしたいと考えております。
些細なものでも構いませんので、沢山のフィードバックをお待ちしております。
なお、esa API v1のβ期間は2015-08-31までを予定しています。
APIとの通信には HTTPS プロトコルを使用します。アクセス先のホストは api.esa.io です。
API v1へのリクエストには、 GET / POST / PUT / PATCH / DELETE の HTTPメソッドを利用します。
多くのAPIはリクエスト時にパラメータを含められますが、GETリクエストにはURIクエリ文字列を利用し、それ以外の場合にはリクエストボディを利用します。パラメータにはページネーション等の任意で渡すものと、記事名等の必須のものが存在します。
現時点では、ユーザ毎に15分間に75リクエストまで受け付けます。βテスト期間を通じて適切な値に調整したいと考えています。
制限状況はAPIのレスポンスヘッダに含まれる X-Ratelimit-* の値をご確認下さい。
X-Ratelimit-Limit: 75
X-Ratelimit-Remaining: 73
なお、決められた制限を超える場合は、429 Too Many Requests が返却されます。
200 / 201 / 204 / 400 / 401 / 402 / 403 / 404 / 500 のステータスコードを利用します。
APIとのデータの送受信にはJSONを利用します。JSONをリクエストボディに含める場合はContent-Type: application/jsonをリクエストヘッダに追加して下さい。
エラーが発生した場合は、エラーを表現するオブジェクトを含んだエラーレスポンスが返却されます。
{
  "error": "not_found",
  "message": "Not found"
}
リストを返すAPIでは、すべての要素を一度に返すことは現実的ではないため、ページを指定することができるようになっています。これらのAPIには、1から始まるページ番号を表す page パラメータと、1ページあたりに含まれる要素数を表す per_page パラメータを指定することができます。page の初期値は 1 に設定されています。また、 per_page の初期値は 20、最大値は 100 に設定されています。
ページを指定できるAPIでは、ページネーションの情報を含んだレスポンスを返します。
{
    "teams": [
        ...
    ],
    "prev_page": null,
    "next_page": 2,
    "total_count": 30
}
現時点で以下のライブラリが利用可能です。
esa APIを利用するには、アクセストークンをリクエストに含める必要があります。アクセストークンは、ユーザの管理画面(https://[team].esa.io/user/tokens)から発行できます。
※ private βでは一部のチームでのみトークンが発行可能です。
※ OAuthを利用した認可フローは今後対応予定です。
アクセストークンは、以下のようにリクエストヘッダに含められます。
Authorization: Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
また、URIクエリ文字列として指定することも可能です。
api.esa.io/v1/teams?access_token=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
個々のアクセストークンには、複数のスコープを紐付けることができます。アクセストークンが適切なスコープを持っている時のみAPIを利用することができます。esa APIでは以下のスコープを利用することができます。
| スコープ | 説明 | 
|---|---|
| read | データの読み出し | 
| write | データの書き込み | 
今後実装予定です。
esa上で所属しているチームを表します。
MEMO: member_count等の情報を追加予定
GET /v1/teams HTTP/1.1
{
  "teams": [
    {
      "name": "docs",
      "privacy": "open",
      "description": "esa.io official documents",
      "icon": "https://img.esa.io/uploads/production/teams/105/icon/thumb_m_0537ab827c4b0c18b60af6cdd94f239c.png",
      "url": "https://docs.esa.io/"
    }
  ],
  "prev_page": null,
  "next_page": null,
  "total_count": 1
}
GET /v1/teams/docs HTTP/1.1
{
  "name": "docs",
  "privacy": "open",
  "description": "esa.io official documents",
  "icon": "https://img.esa.io/uploads/production/teams/105/icon/thumb_m_0537ab827c4b0c18b60af6cdd94f239c.png",
  "url": "https://docs.esa.io/"
}
ユーザが作成した記事を表します。
<h1>Getting Started</h1>
created_by と共通です。記事の一覧を更新日の降順で返却します
GET /v1/teams/docs/posts HTTP/1.1
{
  "posts": [
    {
      "number": 1,
      "name": "hi!",
      "full_name": "日報/2015/05/09/hi! #api #dev",
      "wip": true,
      "body_md": "# Getting Started",
      "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
      "created_at": "2015-05-09T11:54:50+09:00",
      "message": "Add Getting Started section",
      "url": "https://docs.esa.io/posts/1",
      "updated_at": "2015-05-09T11:54:51+09:00",
      "tags": [
        "api",
        "dev"
      ],
      "category": "日報/2015/05/09",
      "revision_number": 1,
      "created_by": {
        "name": "Atsuo Fukaya",
        "screen_name": "fukayatsu",
        "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
      },
      "updated_by": {
        "name": "Atsuo Fukaya",
        "screen_name": "fukayatsu",
        "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
      }
    }
  ],
  "prev_page": null,
  "next_page": null,
  "total_count": 1
}
指定された投稿を取得します。
GET /v1/teams/docs/posts/1 HTTP/1.1
{
  "number": 1,
  "name": "hi!",
  "full_name": "日報/2015/05/09/hi! #api #dev",
  "wip": true,
  "body_md": "# Getting Started",
  "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
  "created_at": "2015-05-09T11:54:50+09:00",
  "message": "Add Getting Started section",
  "url": "https://docs.esa.io/posts/1",
  "updated_at": "2015-05-09T11:54:51+09:00",
  "tags": [
    "api",
    "dev"
  ],
  "category": "日報/2015/05/09",
  "revision_number": 1,
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "updated_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "kind": "flow",
  "comments_count": 1,
  "tasks_count": 1,
  "done_tasks_count": 1,
  "stargazers_count": 1,
  "watchers_count": 1,
  "star": true,
  "watch": true
}
記事を新たに投稿します。
POST /v1/teams/docs/posts HTTP/1.1
Content-Type: application/json
{
   "post":{
      "name":"hi!",
      "body_md":"# Getting Started\n",
      "tags":[
         "api",
         "dev"
      ],
      "category":"dev/2015/05/10",
      "wip":false,
      "message":"Add Getting Started section"
   }
}
HTTP/1.1 201 Created
Content-Type: application/json
{
  "number": 5,
  "name": "hi!",
  "full_name": "dev/2015/05/10/hi! #api #dev",
  "wip": false,
  "body_md": "# Getting Started\n",
  "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
  "created_at": "2015-05-09T12:12:37+09:00",
  "message": "Add Getting Started section",
  "url": "https://docs.esa.io/posts/5",
  "updated_at": "2015-05-09T12:12:37+09:00",
  "tags": [
    "api",
    "dev"
  ],
  "category": "dev/2015/05/10",
  "revision_number": 1,
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "updated_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "kind": "flow",
  "comments_count": 0,
  "tasks_count": 0,
  "done_tasks_count": 0,
  "stargazers_count": 0,
  "watchers_count": 1,
  "star": false,
  "watch": false
}
指定された投稿を編集します。
リクエストに正常な post.body_md パラメータと post.original_revision.* パラメータが存在する場合、記事更新時に3 way mergeが行われます。original_revisionパラメータが存在しない場合は、変更は常に後勝ちになります。
PATCH /v1/teams/docs/posts/5 HTTP/1.1
Content-Type: application/json
{
   "post":{
      "name":"hi!",
      "body_md":"# Getting Started\n",
      "tags":[
         "api",
         "dev"
      ],
      "category":"dev/2015/05/10",
      "wip":false,
      "message":"Add Getting Started section",
      "original_revision": {
          "body_md": "# Getting ...",
          "number":1,
          "user": "fukayatsu"
        }
    }
}
HTTP/1.1 200 OK
Content-Type: application/json
{
  "number": 5,
  "name": "hi!",
  "full_name": "日報/2015/05/10/hi! #api #dev",
  "wip": false,
  "body_md": "# Getting Started\n",
  "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
  "created_at": "2015-05-09T12:12:37+09:00",
  "message": "Add Getting Started section",
  "url": "https://docs.esa.io/posts/5",
  "updated_at": "2015-05-09T12:19:48+09:00",
  "tags": [
    "api",
    "dev"
  ],
  "category": "日報/2015/05/10",
  "revision_number": 2,
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "updated_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "overlapped": false,
  "kind": "flow",
  "comments_count": 0,
  "tasks_count": 0,
  "done_tasks_count": 0,
  "stargazers_count": 0,
  "watchers_count": 1,
  "star": false,
  "watch": false
}
true になります。指定された投稿を削除します。
DELETE /v1/teams/docs/posts/5 HTTP/1.1
HTTP/1.1 204 No Content
ユーザが作成したコメントを表します。
LGTM!
<p>LGTM!</p>
2014-05-10T12:45:42+09:00
2014-05-18T23:02:29+09:00
記事のコメント一覧を更新日の降順で返却します。
GET /v1/teams/docs/posts/2/comments HTTP/1.1
{
  "comments": [
    {
      "id": 1,
      "body_md": "(大事)",
      "body_html": "<p>(大事)</p>",
      "created_at": "2014-05-10T12:45:42+09:00",
      "updated_at": "2014-05-18T23:02:29+09:00",
      "url": "https://docs.esa.io/posts/2#comment-1",
      "created_by": {
        "name": "Atsuo Fukaya",
        "screen_name": "fukayatsu",
        "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
      }
    }
  ],
  "prev_page": null,
  "next_page": null,
  "total_count": 1
}
指定されたコメントを取得します。
GET /v1/teams/docs/comments/13 HTTP/1.1
{
  "id": 13,
  "body_md": "読みたい",
  "body_html": "<p>読みたい</p>",
  "created_at": "2014-05-13T16:17:42+09:00",
  "updated_at": "2014-05-18T23:02:29+09:00",
  "url": "https://docs.esa.io/posts/13#comment-13",
  "created_by": {
    "name": "TAEKO AKATSUKA",
    "screen_name": "taea",
    "icon": "https://img.esa.io/uploads/production/users/2/icon/thumb_m_2690997f07b7de3014a36d90827603d6.jpg"
  }
}
記事に新しいコメントを作成します。
POST /v1/teams/docs/posts/2/comments HTTP/1.1
Content-Type: application/json
{"comment":{"body_md":"LGTM!"}}
HTTP/1.1 201 Created
Content-Type: application/json
{
  "id": 22767,
  "body_md": "LGTM!",
  "body_html": "<p>LGTM!</p>\n",
  "created_at": "2015-06-21T19:36:20+09:00",
  "updated_at": "2015-06-21T19:36:20+09:00",
  "url": "https://docs.esa.io/posts/2#comment-22767",
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  }
}
指定されたコメントを更新します。
PATCH /v1/teams/docs/comments/22767 HTTP/1.1
Content-Type: application/json
{"comment":{"body_md":"LGTM!!!"}}
HTTP/1.1 200 OK
Content-Type: application/json
{
  "id": 22767,
  "body_md": "LGTM! :sushi:",
  "body_html": "<p>LGTM!!!</p>\n",
  "created_at": "2015-06-21T19:36:20+09:00",
  "updated_at": "2015-06-21T19:40:33+09:00",
  "url": "https://docs.esa.io/posts/2#comment-22767",
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  }
}
指定されたコメントを削除します。
DELETE /v1/teams/docs/comments/22767 HTTP/1.1
HTTP/1.1 204 No Content
require 'esa'
require 'json'
require 'pp'
class Importer
  def initialize(client, file_path)
    @client = client
    @items  = JSON.parse(File.read(file_path))
  end
  attr_accessor :client, :items
  def wait_for(seconds)
    (seconds / 10).times do
      print '.'
      sleep 10
    end
    puts
  end
  def import(dry_run: true, start_index: 0)
    users_map = {
      "fukayatsu" => 'fukayatsu',
      "ken_c_lo"  => 'taea',
      "foo"       => 'bar',
    }
    items.sort_by{ |item| item['updated_at'] }.each.with_index do |item, index|
      next unless index >= start_index
      params = {
        name:     item['title'],
        category: "Imports/Qiita",
        tags:     item['tags'].map{ |tag| tag['name'].gsub('/', '-') },
        body_md:  <<-BODY_MD,
created_at: #{item['created_at']}
user: #{item['user']['url_name']}
#{item['raw_body']}
BODY_MD
        wip:      false,
        message:  '[skip notice] Imported from Qiita',
        user:     users_map[item['user']['url_name']] || 'esa_bot',  # 記事作成者上書き: owner権限が必要
      }
      if dry_run
        puts "***** index: #{index} *****"
        pp params
        puts
        next
      end
      print "[#{Time.now}] index[#{index}] #{item['title']} => "
      response = client.create_post(params)
      case response.status
      when 201
        puts "created: #{response.body["full_name"]}"
      when 429
        retry_after = (response.headers['Retry-After'] || 20 * 60).to_i
        puts "rate limit exceeded: will retry after #{retry_after} seconds."
        wait_for(retry_after)
        redo
      else
        puts "failure with status: #{response.status}"
        exit 1
      end
    end
  end
end
client = Esa::Client.new(
  access_token: 'xxxxx', 
  current_team: 'your-team-name', # 移行先のチーム名(サブドメイン)
)
importer = Importer.new(client, '/path/to/exported_from_qiita_team.json')
# dry_run: trueで確認後に dry_run: falseで実際にimportを実行
importer.import(dry_run: true, start_index: 0) 
require 'esa'
require 'pp'
class Importer
  def initialize(client, dir_path)
    @client = client
    @files = Dir.glob File.expand_path(File.join(dir_path, '*.md'))
  end
  attr_accessor :client, :files
  def wait_for(seconds)
    (seconds / 10).times do
      print '.'
      sleep 10
    end
    puts
  end
  def import(dry_run: true, start_index: 0)
     files.sort_by { |file| File.basename(file, '.*').to_i }.each.with_index do |file, index|
      next unless index >= start_index
      params = {
        name:     File.basename(file, '.*'),
        category: "Imports/DocBase",
        body_md:  File.read(file),
        wip:      false,
        message:  '[skip notice] Imported from DocBase',
        user:     'esa_bot',  # 記事作成者上書き: owner権限が必要
      }
      if dry_run
        puts "***** index: #{index} *****"
        pp params
        puts
        next
      end
      print "[#{Time.now}] index[#{index}] #{params['name']} => "
      response = client.create_post(params)
      case response.status
      when 201
        puts "created: #{response.body["full_name"]}"
      when 429
        retry_after = (response.headers['Retry-After'] || 20 * 60).to_i
        puts "rate limit exceeded: will retry after #{retry_after} seconds."
        wait_for(retry_after)
        redo
      else
        puts "failure with status: #{response.status}"
        exit 1
      end
    end
  end
end
client = Esa::Client.new(
  access_token: 'xxxxx',
  current_team: 'your-team-name', # 移行先のチーム名(サブドメイン)
)
importer = Importer.new(client, '/path/to/export_dir') # docbaseからexportしたzipを解答してできたフォルダのpath
# dry_run: trueで確認後に dry_run: falseで実際にimportを実行
importer.import(dry_run: true, start_index: 0)
# はじめに
この記事では esa API v1について説明します。
esa API v1は現在public βの状態にあります。
ベータ期間中もできるだけ非互換の変更は避けるつもりですが、APIの仕様を変更することがありますので適宜この記事を確認して下さい。
β期間中にフィードバックを頂いて、esa APIおよびAPIドキュメントをより良いものにしたいと考えております。
些細なものでも構いませんので、沢山のフィードバックをお待ちしております。
なお、esa API v1のβ期間は2015-08-31までを予定しています。
# 概要
## リクエスト
APIとの通信には `HTTPS` プロトコルを使用します。アクセス先のホストは `api.esa.io` です。
## パラメータ
API v1へのリクエストには、 `GET` / `POST` / `PUT` / `PATCH` / `DELETE` の HTTPメソッドを利用します。
多くのAPIはリクエスト時にパラメータを含められますが、GETリクエストにはURIクエリ文字列を利用し、それ以外の場合にはリクエストボディを利用します。パラメータにはページネーション等の任意で渡すものと、記事名等の必須のものが存在します。
## 利用制限
現時点では、ユーザ毎に15分間に75リクエストまで受け付けます。βテスト期間を通じて適切な値に調整したいと考えています。
制限状況はAPIのレスポンスヘッダに含まれる `X-Ratelimit-*` の値をご確認下さい。
```
X-Ratelimit-Limit: 75
X-Ratelimit-Remaining: 73
```
なお、決められた制限を超える場合は、`429 Too Many Requests` が返却されます。
## ステータスコード
`200` / `201` / `204` / `400` / `401` / `402` / `403` / `404` / `500` のステータスコードを利用します。
## データ形式
APIとのデータの送受信にはJSONを利用します。JSONをリクエストボディに含める場合は`Content-Type: application/json`をリクエストヘッダに追加して下さい。
## エラーレスポンス
エラーが発生した場合は、エラーを表現するオブジェクトを含んだエラーレスポンスが返却されます。
```json
{
  "error": "not_found",
  "message": "Not found"
}
```
## ページネーション
リストを返すAPIでは、すべての要素を一度に返すことは現実的ではないため、ページを指定することができるようになっています。これらのAPIには、1から始まるページ番号を表す `page` パラメータと、1ページあたりに含まれる要素数を表す `per_page` パラメータを指定することができます。`page` の初期値は `1` に設定されています。また、 `per_page` の初期値は `20`、最大値は `100` に設定されています。
ページを指定できるAPIでは、ページネーションの情報を含んだレスポンスを返します。
```
{
    "teams": [
        ...
    ],
    "prev_page": null,
    "next_page": 2,
    "total_count": 30
}
```
## クライアントライブラリ
現時点で以下のライブラリが利用可能です。
- Ruby
    - [esaio/esa-ruby](https://github.com/esaio/esa-ruby)
# 認証と認可
esa APIを利用するには、アクセストークンをリクエストに含める必要があります。アクセストークンは、ユーザの管理画面(https://[team].esa.io/user/tokens)から発行できます。
※ private βでは一部のチームでのみトークンが発行可能です。
※ OAuthを利用した認可フローは今後対応予定です。
## トークン
アクセストークンは、以下のようにリクエストヘッダに含められます。
```
Authorization: Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
```
また、URIクエリ文字列として指定することも可能です。
```
api.esa.io/v1/teams?access_token=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
```
## スコープ
個々のアクセストークンには、複数のスコープを紐付けることができます。アクセストークンが適切なスコープを持っている時のみAPIを利用することができます。esa APIでは以下のスコープを利用することができます。
| スコープ | 説明 |
| --- | --- |
| read | データの読み出し |
| write | データの書き込み |
## OAuth
今後実装予定です。
# チーム
esa上で所属しているチームを表します。
- name (String)
    - チームを特定するための一意なIDです。サブドメインになります。
    - Example: "docs"
- privacy (String)
    - チームの公開範囲です。
        - closed: "チームメンバーだけが情報にアクセスできます。"
        - open: "ShipItされた記事はインターネット上に公開されます。"
- description (String)
    - チームの説明です
    - Example: "esa.io official documents"
- icon (String)
    - チームのアイコンです
    - Example: "https://img.esa.io/uploads/production/teams/105/icon/thumb_m_0537ab827c4b0c18b60af6cdd94f239c.png"
MEMO: member_count等の情報を追加予定
## GET /api/v1/teams
```
GET /v1/teams HTTP/1.1
```
```
{
  "teams": [
    {
      "name": "docs",
      "privacy": "open",
      "description": "esa.io official documents",
      "icon": "https://img.esa.io/uploads/production/teams/105/icon/thumb_m_0537ab827c4b0c18b60af6cdd94f239c.png",
      "url": "https://docs.esa.io/"
    }
  ],
  "prev_page": null,
  "next_page": null,
  "total_count": 1
}
```
## GET /api/v1/teams/:team_name
```
GET /v1/teams/docs HTTP/1.1
```
```
{
  "name": "docs",
  "privacy": "open",
  "description": "esa.io official documents",
  "icon": "https://img.esa.io/uploads/production/teams/105/icon/thumb_m_0537ab827c4b0c18b60af6cdd94f239c.png",
  "url": "https://docs.esa.io/"
}
```
# 記事
ユーザが作成した記事を表します。
- number (Integer)
    - チーム内で記事を特定するためのIDです
    - Example: 123
- name (String)
    - 記事名です。タグやカテゴリー部分は含みません。
    - Example: "hi!"
- tags (Array of String)
    - 記事に紐付けられたタグです。
    - Example: ["api", "dev"]
- category (String)
    - 記事が属するカテゴリです
    - Example: "日報/2015/05/09"
- full_name (String)
    - カテゴリとタグを含む、記事名です。
    - Example: "日報/2015/05/09/hi! #api #dev"
- wip (Boolean)
    - 記事がWIP(Working In Progress)状態かどうかを表します。
    - Example: "true"
- body_md (String)
    - Markdownで書かれた記事の本文です
    - Example "# Getting Started"
- body_html (String)
    - HTMLに変換された記事の本文です。
    - Example: `<h1>Getting Started</h1>`
- created_at (DateTime String)
    - 記事が作成された日時です
    - Example: "2014-05-10T12:08:55+09:00"
- updated_at (DateTime String)
    - 記事が更新された日時です。
    - Example: "2014-05-11T19:21:00+09:00"
- message (String)
    - 記事更新時の変更メモです。
    - Example: "Add Getting Started section"
- revision_number(Integer)
    - 記事のリビジョン番号です。
    - Example: 47
- created_by (Object)
    - 記事を作成したユーザを表します。
    - name (String)
        - ユーザ名です。
        - Example: "Atsuo Fukaya"
    - screen_name (String)
        - ユーザを一意に識別するIDです。
        - Example: "fukayatsu"
    - icon (String)
        - ユーザのアイコンのURLです。
        - Exmaple: "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
- updated_by (Object)
    - 記事を最後に更新したユーザを表します。フィールドは `created_by` と共通です。
    - name (String)
    - screen_name (String)
    - icon (String)
- kind(String, "stock" or "flow")
    - 記事の種別を表します
- comments_count(Integer)
    - 記事へのコメント数を表します
    - Example: 1
- tasks_count(Integer)
    - 記事中のタスクの総数を表します
    - Example: 1
- done_tasks_count(Integer)
    - 記事中の完了済みのタスクの総数を表します
    - Example: 1
- stargazers_count(Integer)
    - 記事にStarをしている人数を表します
    - Example: 1
- watchers_count(Integer)
    - 記事をWatchしている人数を表します
    - Example: 1
- star(Boolean)
    - ユーザーが記事をStarしているかどうかを表します
    - Example: true
- watch(Boolean)
    - ユーザーが記事をWatchしているかどうかを表します
    - Example: true
## GET /api/v1/teams/:team_name/posts
記事の一覧を更新日の降順で返却します
```
GET /v1/teams/docs/posts HTTP/1.1
```
### URIクエリ文字列
- q (String)
    - 記事を絞り込むための条件を指定します
    - [#104:  help/記事の検索方法](/posts/104) を参照
```
{
  "posts": [
    {
      "number": 1,
      "name": "hi!",
      "full_name": "日報/2015/05/09/hi! #api #dev",
      "wip": true,
      "body_md": "# Getting Started",
      "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
      "created_at": "2015-05-09T11:54:50+09:00",
      "message": "Add Getting Started section",
      "url": "https://docs.esa.io/posts/1",
      "updated_at": "2015-05-09T11:54:51+09:00",
      "tags": [
        "api",
        "dev"
      ],
      "category": "日報/2015/05/09",
      "revision_number": 1,
      "created_by": {
        "name": "Atsuo Fukaya",
        "screen_name": "fukayatsu",
        "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
      },
      "updated_by": {
        "name": "Atsuo Fukaya",
        "screen_name": "fukayatsu",
        "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
      }
    }
  ],
  "prev_page": null,
  "next_page": null,
  "total_count": 1
}
```
## GET /api/v1/teams/:team_name/posts/:post_number
指定された投稿を取得します。
```
GET /v1/teams/docs/posts/1 HTTP/1.1
```
```
{
  "number": 1,
  "name": "hi!",
  "full_name": "日報/2015/05/09/hi! #api #dev",
  "wip": true,
  "body_md": "# Getting Started",
  "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
  "created_at": "2015-05-09T11:54:50+09:00",
  "message": "Add Getting Started section",
  "url": "https://docs.esa.io/posts/1",
  "updated_at": "2015-05-09T11:54:51+09:00",
  "tags": [
    "api",
    "dev"
  ],
  "category": "日報/2015/05/09",
  "revision_number": 1,
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "updated_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "kind": "flow",
  "comments_count": 1,
  "tasks_count": 1,
  "done_tasks_count": 1,
  "stargazers_count": 1,
  "watchers_count": 1,
  "star": true,
  "watch": true
}
```
## POST /api/v1/teams/:team_name/posts
記事を新たに投稿します。
- post (Object)
    - name (String, **required**)
    - body_md (String)
    - tags (Array of String)
    - category (String)
    - wip (Boolean, default: true)
    - message (String)
    - user(String)
        - チームメンバーのscreen_nameもしくは "esa_bot" を指定することで記事の投稿者を上書きすることができます。
        - このパラメータは **team の owner** だけ が使用することができます。
```
POST /v1/teams/docs/posts HTTP/1.1
Content-Type: application/json
{
   "post":{
      "name":"hi!",
      "body_md":"# Getting Started\n",
      "tags":[
         "api",
         "dev"
      ],
      "category":"dev/2015/05/10",
      "wip":false,
      "message":"Add Getting Started section"
   }
}
```
```
HTTP/1.1 201 Created
Content-Type: application/json
{
  "number": 5,
  "name": "hi!",
  "full_name": "dev/2015/05/10/hi! #api #dev",
  "wip": false,
  "body_md": "# Getting Started\n",
  "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
  "created_at": "2015-05-09T12:12:37+09:00",
  "message": "Add Getting Started section",
  "url": "https://docs.esa.io/posts/5",
  "updated_at": "2015-05-09T12:12:37+09:00",
  "tags": [
    "api",
    "dev"
  ],
  "category": "dev/2015/05/10",
  "revision_number": 1,
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "updated_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "kind": "flow",
  "comments_count": 0,
  "tasks_count": 0,
  "done_tasks_count": 0,
  "stargazers_count": 0,
  "watchers_count": 1,
  "star": false,
  "watch": false
}
```
## PATCH /api/v1/teams/:team_name/posts/:post_number
指定された投稿を編集します。
- post (Object)
    - name (String)
    - body_md (String)
    - tags (Array of String)
    - category (String)
    - wip (Boolean)
    - message (String)
    - created_by(String)
        - チームメンバーのscreen_nameもしくは "esa_bot" を指定することで記事の **作成者** を上書きすることができます。
        - このパラメータは **team の owner** だけ が使用することができます。
    - updated_by(String)
        - チームメンバーのscreen_nameもしくは "esa_bot" を指定することで記事の **更新者** を上書きすることができます。
        - このパラメータは **team の owner** だけ が使用することができます。
    - original_revision (Object)
        - body_md (String)
            - 変更前の記事の本文です
        - number (Integer)
            -  変更前の記事のrevision_numberを指定します
        - user (String)
            - 変更前の記事の最終更新者のscreen_nameを指定します
### original_revisionについて
リクエストに正常な `post.body_md` パラメータと `post.original_revision.*` パラメータが存在する場合、記事更新時に3 way mergeが行われます。original_revisionパラメータが存在しない場合は、変更は常に後勝ちになります。
> [release_note/2014/12/23/記事保存時の自動マージ - docs.esa.io](https://docs.esa.io/posts/35)
```
PATCH /v1/teams/docs/posts/5 HTTP/1.1
Content-Type: application/json
{
   "post":{
      "name":"hi!",
      "body_md":"# Getting Started\n",
      "tags":[
         "api",
         "dev"
      ],
      "category":"dev/2015/05/10",
      "wip":false,
      "message":"Add Getting Started section",
      "original_revision": {
          "body_md": "# Getting ...",
          "number":1,
          "user": "fukayatsu"
        }
    }
}
```
```
HTTP/1.1 200 OK
Content-Type: application/json
{
  "number": 5,
  "name": "hi!",
  "full_name": "日報/2015/05/10/hi! #api #dev",
  "wip": false,
  "body_md": "# Getting Started\n",
  "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
  "created_at": "2015-05-09T12:12:37+09:00",
  "message": "Add Getting Started section",
  "url": "https://docs.esa.io/posts/5",
  "updated_at": "2015-05-09T12:19:48+09:00",
  "tags": [
    "api",
    "dev"
  ],
  "category": "日報/2015/05/10",
  "revision_number": 2,
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "updated_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "overlapped": false,
  "kind": "flow",
  "comments_count": 0,
  "tasks_count": 0,
  "done_tasks_count": 0,
  "stargazers_count": 0,
  "watchers_count": 1,
  "star": false,
  "watch": false
}
```
- overlapped (Boolean)
    - 3 way mergeを行いコンフリクトが起きた場合にのみ `true` になります。
## DELETE /api/v1/teams/:team_name/posts/:post_number
指定された投稿を削除します。
```
DELETE /v1/teams/docs/posts/5 HTTP/1.1
```
```
HTTP/1.1 204 No Content
```
# コメント
ユーザが作成したコメントを表します。
- id
    - コメントを一意に識別するIDです
    - Example: 123
- body_md (String)
    - Markdownで書かれたコメントの本文です
    -  Example: `LGTM!`
- body_html (String)
    - HTMLに変換されたコメントの本文です
    - Example: `<p>LGTM!</p>`
- created_at (DateTime String)
    - コメントが作成された日時です
    - Example: `2014-05-10T12:45:42+09:00`
- updated_at (DateTime String)
    - コメントが更新された日時です
    - Example: `2014-05-18T23:02:29+09:00`
- url (String)
    - コメントのpermalinkです
    - https://docs.esa.io/posts/2#comment-123
- created_by (Object)
    - コメントを作成したユーザを表します。
    - name (String)
        - ユーザ名です。
        - Example: "Atsuo Fukaya"
    - screen_name (String)
        - ユーザを一意に識別するIDです。
        - Example: "fukayatsu"
    - icon (String)
        - ユーザのアイコンのURLです。
        - Exmaple: "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
## GET /api/v1/teams/:team_name/posts/:post_number/comments
記事のコメント一覧を更新日の降順で返却します。
```
GET /v1/teams/docs/posts/2/comments HTTP/1.1
```
```
{
  "comments": [
    {
      "id": 1,
      "body_md": "(大事)",
      "body_html": "<p>(大事)</p>",
      "created_at": "2014-05-10T12:45:42+09:00",
      "updated_at": "2014-05-18T23:02:29+09:00",
      "url": "https://docs.esa.io/posts/2#comment-1",
      "created_by": {
        "name": "Atsuo Fukaya",
        "screen_name": "fukayatsu",
        "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
      }
    }
  ],
  "prev_page": null,
  "next_page": null,
  "total_count": 1
}
```
## GET /api/v1/teams/:team_name/comments/:comment_id
指定されたコメントを取得します。
```
GET /v1/teams/docs/comments/13 HTTP/1.1
```
```
{
  "id": 13,
  "body_md": "読みたい",
  "body_html": "<p>読みたい</p>",
  "created_at": "2014-05-13T16:17:42+09:00",
  "updated_at": "2014-05-18T23:02:29+09:00",
  "url": "https://docs.esa.io/posts/13#comment-13",
  "created_by": {
    "name": "TAEKO AKATSUKA",
    "screen_name": "taea",
    "icon": "https://img.esa.io/uploads/production/users/2/icon/thumb_m_2690997f07b7de3014a36d90827603d6.jpg"
  }
}
```
## POST /api/v1/teams/:team_name/posts/:post_number/comments
記事に新しいコメントを作成します。
- comment (Object)
    - body_md (String, **required**)
    - user(String)
        - チームメンバーのscreen_nameもしくは "esa_bot" を指定することで記事の投稿者を上書きすることができます。
        - このパラメータは **team の owner** だけ が使用することができます。
```
POST /v1/teams/docs/posts/2/comments HTTP/1.1
Content-Type: application/json
{"comment":{"body_md":"LGTM!"}}
```
```
HTTP/1.1 201 Created
Content-Type: application/json
{
  "id": 22767,
  "body_md": "LGTM!",
  "body_html": "<p>LGTM!</p>\n",
  "created_at": "2015-06-21T19:36:20+09:00",
  "updated_at": "2015-06-21T19:36:20+09:00",
  "url": "https://docs.esa.io/posts/2#comment-22767",
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  }
}
```
## PATCH /api/v1/teams/:team_name/comments/:comment_id
指定されたコメントを更新します。
```
PATCH /v1/teams/docs/comments/22767 HTTP/1.1
Content-Type: application/json
{"comment":{"body_md":"LGTM!!!"}}
```
```
HTTP/1.1 200 OK
Content-Type: application/json
{
  "id": 22767,
  "body_md": "LGTM! :sushi:",
  "body_html": "<p>LGTM!!!</p>\n",
  "created_at": "2015-06-21T19:36:20+09:00",
  "updated_at": "2015-06-21T19:40:33+09:00",
  "url": "https://docs.esa.io/posts/2#comment-22767",
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  }
}
```
## DELETE /api/v1/teams/:team_name/comments/:comment_id
指定されたコメントを削除します。
```
DELETE /v1/teams/docs/comments/22767 HTTP/1.1
```
```
HTTP/1.1 204 No Content
```
# 他サービスからのインポートスクリプトのサンプル
## Qiita:Team
```import.rb
require 'esa'
require 'json'
require 'pp'
 
class Importer
  def initialize(client, file_path)
    @client = client
    @items  = JSON.parse(File.read(file_path))
  end
  attr_accessor :client, :items
 
  def wait_for(seconds)
    (seconds / 10).times do
      print '.'
      sleep 10
    end
    puts
  end
 
  def import(dry_run: true, start_index: 0)
    users_map = {
      "fukayatsu" => 'fukayatsu',
      "ken_c_lo"  => 'taea',
      "foo"       => 'bar',
    }
    items.sort_by{ |item| item['updated_at'] }.each.with_index do |item, index|
      next unless index >= start_index
 
      params = {
        name:     item['title'],
        category: "Imports/Qiita",
        tags:     item['tags'].map{ |tag| tag['name'].gsub('/', '-') },
        body_md:  <<-BODY_MD,
created_at: #{item['created_at']}
user: #{item['user']['url_name']}
#{item['raw_body']}
BODY_MD
        wip:      false,
        message:  '[skip notice] Imported from Qiita',
        user:     users_map[item['user']['url_name']] || 'esa_bot',  # 記事作成者上書き: owner権限が必要
      }
 
      if dry_run
        puts "***** index: #{index} *****"
        pp params
        puts
        next
      end
 
      print "[#{Time.now}] index[#{index}] #{item['title']} => "
      response = client.create_post(params)
      case response.status
      when 201
        puts "created: #{response.body["full_name"]}"
      when 429
        retry_after = (response.headers['Retry-After'] || 20 * 60).to_i
        puts "rate limit exceeded: will retry after #{retry_after} seconds."
        wait_for(retry_after)
        redo
      else
        puts "failure with status: #{response.status}"
        exit 1
      end
    end
  end
end
 
client = Esa::Client.new(
  access_token: 'xxxxx', 
  current_team: 'your-team-name', # 移行先のチーム名(サブドメイン)
)
importer = Importer.new(client, '/path/to/exported_from_qiita_team.json')
# dry_run: trueで確認後に dry_run: falseで実際にimportを実行
importer.import(dry_run: true, start_index: 0) 
```
## DocBase
```ruby:import.rb
require 'esa'
require 'pp'
class Importer
  def initialize(client, dir_path)
    @client = client
    @files = Dir.glob File.expand_path(File.join(dir_path, '*.md'))
  end
  attr_accessor :client, :files
  def wait_for(seconds)
    (seconds / 10).times do
      print '.'
      sleep 10
    end
    puts
  end
  def import(dry_run: true, start_index: 0)
     files.sort_by { |file| File.basename(file, '.*').to_i }.each.with_index do |file, index|
      next unless index >= start_index
      params = {
        name:     File.basename(file, '.*'),
        category: "Imports/DocBase",
        body_md:  File.read(file),
        wip:      false,
        message:  '[skip notice] Imported from DocBase',
        user:     'esa_bot',  # 記事作成者上書き: owner権限が必要
      }
      if dry_run
        puts "***** index: #{index} *****"
        pp params
        puts
        next
      end
      print "[#{Time.now}] index[#{index}] #{params['name']} => "
      response = client.create_post(params)
      case response.status
      when 201
        puts "created: #{response.body["full_name"]}"
      when 429
        retry_after = (response.headers['Retry-After'] || 20 * 60).to_i
        puts "rate limit exceeded: will retry after #{retry_after} seconds."
        wait_for(retry_after)
        redo
      else
        puts "failure with status: #{response.status}"
        exit 1
      end
    end
  end
end
client = Esa::Client.new(
  access_token: 'xxxxx',
  current_team: 'your-team-name', # 移行先のチーム名(サブドメイン)
)
importer = Importer.new(client, '/path/to/export_dir') # docbaseからexportしたzipを解答してできたフォルダのpath
# dry_run: trueで確認後に dry_run: falseで実際にimportを実行
importer.import(dry_run: true, start_index: 0)
```
# 今後の実装予定
- Star API
- Watch API
- Preview API
- Revision API
- OAuth
この記事では esa API v1について説明します。
esa API v1は現在public βの状態にあります。
ベータ期間中もできるだけ非互換の変更は避けるつもりですが、APIの仕様を変更することがありますので適宜この記事を確認して下さい。
β期間中にフィードバックを頂いて、esa APIおよびAPIドキュメントをより良いものにしたいと考えております。
些細なものでも構いませんので、沢山のフィードバックをお待ちしております。
なお、esa API v1のβ期間は2015-08-31までを予定しています。
APIとの通信には HTTPS プロトコルを使用します。アクセス先のホストは api.esa.io です。
API v1へのリクエストには、 GET / POST / PUT / PATCH / DELETE の HTTPメソッドを利用します。
多くのAPIはリクエスト時にパラメータを含められますが、GETリクエストにはURIクエリ文字列を利用し、それ以外の場合にはリクエストボディを利用します。パラメータにはページネーション等の任意で渡すものと、記事名等の必須のものが存在します。
現時点では、ユーザ毎に15分間に75リクエストまで受け付けます。βテスト期間を通じて適切な値に調整したいと考えています。
制限状況はAPIのレスポンスヘッダに含まれる X-Ratelimit-* の値をご確認下さい。
X-Ratelimit-Limit: 75
X-Ratelimit-Remaining: 73
なお、決められた制限を超える場合は、429 Too Many Requests が返却されます。
200 / 201 / 204 / 400 / 401 / 402 / 403 / 404 / 500 のステータスコードを利用します。
APIとのデータの送受信にはJSONを利用します。JSONをリクエストボディに含める場合はContent-Type: application/jsonをリクエストヘッダに追加して下さい。
エラーが発生した場合は、エラーを表現するオブジェクトを含んだエラーレスポンスが返却されます。
{
  "error": "not_found",
  "message": "Not found"
}
リストを返すAPIでは、すべての要素を一度に返すことは現実的ではないため、ページを指定することができるようになっています。これらのAPIには、1から始まるページ番号を表す page パラメータと、1ページあたりに含まれる要素数を表す per_page パラメータを指定することができます。page の初期値は 1 に設定されています。また、 per_page の初期値は 20、最大値は 100 に設定されています。
ページを指定できるAPIでは、ページネーションの情報を含んだレスポンスを返します。
{
    "teams": [
        ...
    ],
    "prev_page": null,
    "next_page": 2,
    "total_count": 30
}
現時点で以下のライブラリが利用可能です。
esa APIを利用するには、アクセストークンをリクエストに含める必要があります。アクセストークンは、ユーザの管理画面(https://[team].esa.io/user/tokens)から発行できます。
※ private βでは一部のチームでのみトークンが発行可能です。
※ OAuthを利用した認可フローは今後対応予定です。
アクセストークンは、以下のようにリクエストヘッダに含められます。
Authorization: Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
また、URIクエリ文字列として指定することも可能です。
api.esa.io/v1/teams?access_token=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
個々のアクセストークンには、複数のスコープを紐付けることができます。アクセストークンが適切なスコープを持っている時のみAPIを利用することができます。esa APIでは以下のスコープを利用することができます。
| スコープ | 説明 | 
|---|---|
| read | データの読み出し | 
| write | データの書き込み | 
今後実装予定です。
esa上で所属しているチームを表します。
MEMO: member_count等の情報を追加予定
GET /v1/teams HTTP/1.1
{
  "teams": [
    {
      "name": "docs",
      "privacy": "open",
      "description": "esa.io official documents",
      "icon": "https://img.esa.io/uploads/production/teams/105/icon/thumb_m_0537ab827c4b0c18b60af6cdd94f239c.png",
      "url": "https://docs.esa.io/"
    }
  ],
  "prev_page": null,
  "next_page": null,
  "total_count": 1
}
GET /v1/teams/docs HTTP/1.1
{
  "name": "docs",
  "privacy": "open",
  "description": "esa.io official documents",
  "icon": "https://img.esa.io/uploads/production/teams/105/icon/thumb_m_0537ab827c4b0c18b60af6cdd94f239c.png",
  "url": "https://docs.esa.io/"
}
ユーザが作成した記事を表します。
<h1>Getting Started</h1>
created_by と共通です。記事の一覧を更新日の降順で返却します
GET /v1/teams/docs/posts HTTP/1.1
{
  "posts": [
    {
      "number": 1,
      "name": "hi!",
      "full_name": "日報/2015/05/09/hi! #api #dev",
      "wip": true,
      "body_md": "# Getting Started",
      "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
      "created_at": "2015-05-09T11:54:50+09:00",
      "message": "Add Getting Started section",
      "url": "https://docs.esa.io/posts/1",
      "updated_at": "2015-05-09T11:54:51+09:00",
      "tags": [
        "api",
        "dev"
      ],
      "category": "日報/2015/05/09",
      "revision_number": 1,
      "created_by": {
        "name": "Atsuo Fukaya",
        "screen_name": "fukayatsu",
        "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
      },
      "updated_by": {
        "name": "Atsuo Fukaya",
        "screen_name": "fukayatsu",
        "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
      }
    }
  ],
  "prev_page": null,
  "next_page": null,
  "total_count": 1
}
指定された投稿を取得します。
GET /v1/teams/docs/posts/1 HTTP/1.1
{
  "number": 1,
  "name": "hi!",
  "full_name": "日報/2015/05/09/hi! #api #dev",
  "wip": true,
  "body_md": "# Getting Started",
  "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
  "created_at": "2015-05-09T11:54:50+09:00",
  "message": "Add Getting Started section",
  "url": "https://docs.esa.io/posts/1",
  "updated_at": "2015-05-09T11:54:51+09:00",
  "tags": [
    "api",
    "dev"
  ],
  "category": "日報/2015/05/09",
  "revision_number": 1,
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "updated_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "kind": "flow",
  "comments_count": 1,
  "tasks_count": 1,
  "done_tasks_count": 1,
  "stargazers_count": 1,
  "watchers_count": 1,
  "star": true,
  "watch": true
}
記事を新たに投稿します。
POST /v1/teams/docs/posts HTTP/1.1
Content-Type: application/json
{
   "post":{
      "name":"hi!",
      "body_md":"# Getting Started\n",
      "tags":[
         "api",
         "dev"
      ],
      "category":"dev/2015/05/10",
      "wip":false,
      "message":"Add Getting Started section"
   }
}
HTTP/1.1 201 Created
Content-Type: application/json
{
  "number": 5,
  "name": "hi!",
  "full_name": "dev/2015/05/10/hi! #api #dev",
  "wip": false,
  "body_md": "# Getting Started\n",
  "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
  "created_at": "2015-05-09T12:12:37+09:00",
  "message": "Add Getting Started section",
  "url": "https://docs.esa.io/posts/5",
  "updated_at": "2015-05-09T12:12:37+09:00",
  "tags": [
    "api",
    "dev"
  ],
  "category": "dev/2015/05/10",
  "revision_number": 1,
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "updated_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "kind": "flow",
  "comments_count": 0,
  "tasks_count": 0,
  "done_tasks_count": 0,
  "stargazers_count": 0,
  "watchers_count": 1,
  "star": false,
  "watch": false
}
指定された投稿を編集します。
リクエストに正常な post.body_md パラメータと post.original_revision.* パラメータが存在する場合、記事更新時に3 way mergeが行われます。original_revisionパラメータが存在しない場合は、変更は常に後勝ちになります。
PATCH /v1/teams/docs/posts/5 HTTP/1.1
Content-Type: application/json
{
   "post":{
      "name":"hi!",
      "body_md":"# Getting Started\n",
      "tags":[
         "api",
         "dev"
      ],
      "category":"dev/2015/05/10",
      "wip":false,
      "message":"Add Getting Started section",
      "original_revision": {
          "body_md": "# Getting ...",
          "number":1,
          "user": "fukayatsu"
        }
    }
}
HTTP/1.1 200 OK
Content-Type: application/json
{
  "number": 5,
  "name": "hi!",
  "full_name": "日報/2015/05/10/hi! #api #dev",
  "wip": false,
  "body_md": "# Getting Started\n",
  "body_html": "<h1 id=\"1-0-0\" name=\"1-0-0\">\n<a class=\"anchor\" href=\"#1-0-0\"><i class=\"fa fa-link\"></i><span class=\"hidden\" data-text=\"Getting Started\"> > Getting Started</span></a>Getting Started</h1>\n",
  "created_at": "2015-05-09T12:12:37+09:00",
  "message": "Add Getting Started section",
  "url": "https://docs.esa.io/posts/5",
  "updated_at": "2015-05-09T12:19:48+09:00",
  "tags": [
    "api",
    "dev"
  ],
  "category": "日報/2015/05/10",
  "revision_number": 2,
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "updated_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "http://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  },
  "overlapped": false,
  "kind": "flow",
  "comments_count": 0,
  "tasks_count": 0,
  "done_tasks_count": 0,
  "stargazers_count": 0,
  "watchers_count": 1,
  "star": false,
  "watch": false
}
true になります。指定された投稿を削除します。
DELETE /v1/teams/docs/posts/5 HTTP/1.1
HTTP/1.1 204 No Content
ユーザが作成したコメントを表します。
LGTM!
<p>LGTM!</p>
2014-05-10T12:45:42+09:00
2014-05-18T23:02:29+09:00
記事のコメント一覧を更新日の降順で返却します。
GET /v1/teams/docs/posts/2/comments HTTP/1.1
{
  "comments": [
    {
      "id": 1,
      "body_md": "(大事)",
      "body_html": "<p>(大事)</p>",
      "created_at": "2014-05-10T12:45:42+09:00",
      "updated_at": "2014-05-18T23:02:29+09:00",
      "url": "https://docs.esa.io/posts/2#comment-1",
      "created_by": {
        "name": "Atsuo Fukaya",
        "screen_name": "fukayatsu",
        "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
      }
    }
  ],
  "prev_page": null,
  "next_page": null,
  "total_count": 1
}
指定されたコメントを取得します。
GET /v1/teams/docs/comments/13 HTTP/1.1
{
  "id": 13,
  "body_md": "読みたい",
  "body_html": "<p>読みたい</p>",
  "created_at": "2014-05-13T16:17:42+09:00",
  "updated_at": "2014-05-18T23:02:29+09:00",
  "url": "https://docs.esa.io/posts/13#comment-13",
  "created_by": {
    "name": "TAEKO AKATSUKA",
    "screen_name": "taea",
    "icon": "https://img.esa.io/uploads/production/users/2/icon/thumb_m_2690997f07b7de3014a36d90827603d6.jpg"
  }
}
記事に新しいコメントを作成します。
POST /v1/teams/docs/posts/2/comments HTTP/1.1
Content-Type: application/json
{"comment":{"body_md":"LGTM!"}}
HTTP/1.1 201 Created
Content-Type: application/json
{
  "id": 22767,
  "body_md": "LGTM!",
  "body_html": "<p>LGTM!</p>\n",
  "created_at": "2015-06-21T19:36:20+09:00",
  "updated_at": "2015-06-21T19:36:20+09:00",
  "url": "https://docs.esa.io/posts/2#comment-22767",
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  }
}
指定されたコメントを更新します。
PATCH /v1/teams/docs/comments/22767 HTTP/1.1
Content-Type: application/json
{"comment":{"body_md":"LGTM!!!"}}
HTTP/1.1 200 OK
Content-Type: application/json
{
  "id": 22767,
  "body_md": "LGTM! :sushi:",
  "body_html": "<p>LGTM!!!</p>\n",
  "created_at": "2015-06-21T19:36:20+09:00",
  "updated_at": "2015-06-21T19:40:33+09:00",
  "url": "https://docs.esa.io/posts/2#comment-22767",
  "created_by": {
    "name": "Atsuo Fukaya",
    "screen_name": "fukayatsu",
    "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png"
  }
}
指定されたコメントを削除します。
DELETE /v1/teams/docs/comments/22767 HTTP/1.1
HTTP/1.1 204 No Content
require 'esa'
require 'json'
require 'pp'
class Importer
  def initialize(client, file_path)
    @client = client
    @items  = JSON.parse(File.read(file_path))
  end
  attr_accessor :client, :items
  def wait_for(seconds)
    (seconds / 10).times do
      print '.'
      sleep 10
    end
    puts
  end
  def import(dry_run: true, start_index: 0)
    users_map = {
      "fukayatsu" => 'fukayatsu',
      "ken_c_lo"  => 'taea',
      "foo"       => 'bar',
    }
    items.sort_by{ |item| item['updated_at'] }.each.with_index do |item, index|
      next unless index >= start_index
      params = {
        name:     item['title'],
        category: "Imports/Qiita",
        tags:     item['tags'].map{ |tag| tag['name'].gsub('/', '-') },
        body_md:  <<-BODY_MD,
created_at: #{item['created_at']}
user: #{item['user']['url_name']}
#{item['raw_body']}
BODY_MD
        wip:      false,
        message:  '[skip notice] Imported from Qiita',
        user:     users_map[item['user']['url_name']] || 'esa_bot',  # 記事作成者上書き: owner権限が必要
      }
      if dry_run
        puts "***** index: #{index} *****"
        pp params
        puts
        next
      end
      print "[#{Time.now}] index[#{index}] #{item['title']} => "
      response = client.create_post(params)
      case response.status
      when 201
        puts "created: #{response.body["full_name"]}"
      when 429
        retry_after = (response.headers['Retry-After'] || 20 * 60).to_i
        puts "rate limit exceeded: will retry after #{retry_after} seconds."
        wait_for(retry_after)
        redo
      else
        puts "failure with status: #{response.status}"
        exit 1
      end
    end
  end
end
client = Esa::Client.new(
  access_token: 'xxxxx', 
  current_team: 'your-team-name', # 移行先のチーム名(サブドメイン)
)
importer = Importer.new(client, '/path/to/exported_from_qiita_team.json')
# dry_run: trueで確認後に dry_run: falseで実際にimportを実行
importer.import(dry_run: true, start_index: 0) 
require 'esa'
require 'pp'
class Importer
  def initialize(client, dir_path)
    @client = client
    @files = Dir.glob File.expand_path(File.join(dir_path, '*.md'))
  end
  attr_accessor :client, :files
  def wait_for(seconds)
    (seconds / 10).times do
      print '.'
      sleep 10
    end
    puts
  end
  def import(dry_run: true, start_index: 0)
     files.sort_by { |file| File.basename(file, '.*').to_i }.each.with_index do |file, index|
      next unless index >= start_index
      params = {
        name:     File.basename(file, '.*'),
        category: "Imports/DocBase",
        body_md:  File.read(file),
        wip:      false,
        message:  '[skip notice] Imported from DocBase',
        user:     'esa_bot',  # 記事作成者上書き: owner権限が必要
      }
      if dry_run
        puts "***** index: #{index} *****"
        pp params
        puts
        next
      end
      print "[#{Time.now}] index[#{index}] #{params['name']} => "
      response = client.create_post(params)
      case response.status
      when 201
        puts "created: #{response.body["full_name"]}"
      when 429
        retry_after = (response.headers['Retry-After'] || 20 * 60).to_i
        puts "rate limit exceeded: will retry after #{retry_after} seconds."
        wait_for(retry_after)
        redo
      else
        puts "failure with status: #{response.status}"
        exit 1
      end
    end
  end
end
client = Esa::Client.new(
  access_token: 'xxxxx',
  current_team: 'your-team-name', # 移行先のチーム名(サブドメイン)
)
importer = Importer.new(client, '/path/to/export_dir') # docbaseからexportしたzipを解答してできたフォルダのpath
# dry_run: trueで確認後に dry_run: falseで実際にimportを実行
importer.import(dry_run: true, start_index: 0)