この記事では esa API v1について説明します。
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
をリクエストヘッダに追加して下さい。文字コードはUTF-8を使用して下さい。
エラーが発生した場合は、エラーを表現するオブジェクトを含んだエラーレスポンスが返却されます。
{
"error": "not_found",
"message": "Not found"
}
リストを返すAPIでは、すべての要素を一度に返すことは現実的ではないため、ページを指定することができるようになっています。これらのAPIには、1から始まるページ番号を表す page
パラメータと、1ページあたりに含まれる要素数を表す per_page
パラメータを指定することができます。page
の初期値は 1
に設定されています。また、 per_page
の初期値は 20
、最大値は 100
に設定されています。
ページを指定できるAPIでは、ページネーションの情報を含んだレスポンスを返します。
# GET /v1/teams HTTP/1.1
{
"teams": [
...
],
"prev_page": null,
"next_page": 2,
"total_count": 30,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
現時点で以下のライブラリが利用可能です。
esa APIを利用するには、アクセストークンをリクエストに含める必要があります。アクセストークンは、ユーザの管理画面(https://[team].esa.io/user/tokens)もしくはOAuthを利用した認可フローにより発行できます。
アクセストークンは、以下のようにリクエストヘッダに含められます。
Authorization: Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
また、URIクエリ文字列として指定することも可能です。
api.esa.io/v1/teams?access_token=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
個々のアクセストークンには、複数のスコープを紐付けることができます。アクセストークンが適切なスコープを持っている時のみAPIを利用することができます。esa APIでは以下のスコープを利用することができます。
スコープ | 説明 |
---|---|
read | データの読み出し |
write | データの書き込み |
https://[team].esa.io/user/applications
から アプリケーションを登録できます。
※ RailsやSinatraでは、以下の手順は omniauth-esa を使うことで実装の手間を省くことが出来ます。
アプリケーションのユーザーに認可画面を表示します。
urn:ietf:wg:oauth:2.0:oob
を指定した場合、認可後にリダイレクトせず画面に認証コードを表示しますcode
を指定しますGET /oauth/authorize?client_id=0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&scope=read+write&state=a7e567e2fb858f0e12838798016ee9cf8ccc778 HTTP/1.1
新規にアクセストークンを発行します。
authorization_code
を指定しますPOST /oauth/token HTTP/1.1
{"client_id":"0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04","client_secret":"ca6eb9452f2fdaaa68a8d870bc654db8e6f466f6d42685b914e04c442b2065a2","code":"c486e26492dfdc42bcf4e07bfbd84a97f98548c1cf89178a512e76b79b099313","grant_type":"authorization_code","redirect_uri":"urn:ietf:wg:oauth:2.0:oob"}
HTTP/1.1 200 OK
{
"access_token": "5d49aab1f0796bbbd78100f06c6cd4d667851644012d37421073bb61126cdafc",
"token_type": "bearer",
"scope": "read write",
"created_at": 1461218696
}
アクセストークンの情報を取得します
GET /oauth/token/info HTTP/1.1
Authorization: bearer 5d49aab1f0796bbbd78100f06c6cd4d667851644012d37421073bb61126cdafc
HTTP/1.1 200 OK
{"resource_owner_id":1,"scopes":["read","write"],"expires_in_seconds":null,"application":{"uid":"0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04"},"created_at":1461218696}
アクセストークンを失効させます
POST /oauth/revoke HTTP/1.1
{"client_id":"0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04","client_secret":"ca6eb9452f2fdaaa68a8d870bc654db8e6f466f6d42685b914e04c442b2065a2","token":"5d49aab1f0796bbbd78100f06c6cd4d667851644012d37421073bb61126cdafc"}
HTTP/1.1 200 OK
{}
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,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
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/"
}
チームの統計情報を表します。
GET /v1/teams/foo/stats HTTP/1.1
{
"members": 20,
"posts": 1959,
"posts_wip": 59,
"posts_shipped": 1900,
"comments": 2695,
"stars": 3115,
"daily_active_users": 8,
"weekly_active_users": 14,
"monthly_active_users": 15
}
チームのメンバーを表します。
GET /v1/teams/foo/members HTTP/1.1
{
"members": [
{
"name": "Atsuo Fukaya",
"screen_name": "fukayatsu",
"icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png",
"email": "fukayatsu@esa.io",
"posts_count": 222
},
{
"name": "TAEKO AKATSUKA",
"screen_name": "taea",
"icon": "https://img.esa.io/uploads/production/users/2/icon/thumb_m_2690997f07b7de3014a36d90827603d6.jpg",
"email": "taea@esa.io",
"posts_count": 111
}
],
"prev_page": null,
"next_page": null,
"total_count": 2,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
ユーザが作成した記事を表します。
<h1>Getting Started</h1>
created_by
と共通です。記事の一覧を更新日の降順で返却します
GET /v1/teams/docs/posts HTTP/1.1
comments
を指定するとコメントの配列を含んだレスポンスを返します。stargazers
を指定するとStarの配列を含んだレスポンスを返します。stargazers,comments
のように ,
で区切ることで複数指定できますv2
を指定すると、新しい記事検索エンジンがご利用可能です
updated
(default)
created
stars
watches
comments
best_match
desc
(default)
asc
{
"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,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
指定された投稿を取得します。
comments
を指定するとコメントの配列を含んだレスポンスを返します。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"
},
"stargazers_count": 0,
"star": false
}
],
"prev_page": null,
"next_page": null,
"total_count": 1,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
指定されたコメントを取得します。
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"
},
"stargazers_count": 0,
"star": false
}
記事に新しいコメントを作成します。
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"
},
"stargazers_count": 0,
"star": false
}
指定されたコメントを更新します。
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"
},
"stargazers_count": 0,
"star": false
}
指定されたコメントを削除します。
DELETE /v1/teams/docs/comments/22767 HTTP/1.1
HTTP/1.1 204 No Content
指定された記事にStarをしたユーザ一覧を取得します。
GET /v1/teams/kama/posts/2312/stargazers HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"stargazers": [
{
"created_at": "2016-05-05T11:40:54+09:00",
"body": null,
"user": {
"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,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
指定された記事にStarをします。
POST /v1/teams/kama/posts/2312/star HTTP/1.1
{"body":"foo bar"}
HTTP/1.1 204 No Content
指定された記事へのStarを取り消します。
DELETE /v1/teams/kama/posts/2312/star HTTP/1.1
HTTP/1.1 204 No Content
指定されたコメントにStarをしたユーザ一覧を取得します。
GET /v1/teams/kama/comments/123/stargazers HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"stargazers": [
{
"created_at": "2016-05-05T11:40:54+09:00",
"body": null,
"user": {
"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,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
指定されたコメントにStarをします。
POST /v1/teams/kama/comments/123/star HTTP/1.1
{"body":"foo bar"}
HTTP/1.1 204 No Content
指定されたコメントへのStarを取り消します。
DELETE /v1/teams/kama/comments/123/star HTTP/1.1
HTTP/1.1 204 No Content
指定された記事にWatchをしたユーザ一覧を取得します。
GET /v1/teams/kama/posts/2312/watchers HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"watchers": [
{
"created_at": "2016-05-05T11:40:53+09:00",
"user": {
"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,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
指定された記事にWatchをします。
POST /v1/teams/kama/posts/2312/watch HTTP/1.1
HTTP/1.1 204 No Content
指定された記事のWatchを取り消します。
DELETE /v1/teams/kama/posts/2312/watch HTTP/1.1
HTTP/1.1 204 No Content
現在のアクセストークンで認証中のユーザの情報を表します。
teams
を指定すると所属するチームの配列を含んだレスポンスを返します。GET /v1/user HTTP/1.1
Content-Type: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1,
"name": "Atsuo Fukaya",
"screen_name": "fukayatsu",
"created_at": "2014-05-10T11:50:07+09:00",
"updated_at": "2016-04-17T12:35:16+09:00",
"icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png",
"email": "fukayatsu@esa.io"
}
require 'esa'
require 'json'
require 'pp'
class Importer
def initialize(client, file_path)
@client = client
@exported_data = JSON.parse(File.read(file_path))
return if @exported_data['version'] == 'v1'
puts "This script is for Qiita Export Format v1. Please contact us (hi@esa.io) (\\( ⁰⊖⁰)/)"
exit 1
end
attr_accessor :client, :exported_data
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',
}
exported_data['articles'].sort_by{ |article| article['updated_at'] }.each.with_index do |article, index|
next unless index >= start_index
params = {
name: article['title'],
category: "Imports/Qiita",
tags: article['tags'].map{ |tag| tag['name'].gsub('/', '-') },
body_md: <<-BODY_MD,
created_at: #{article['created_at']}
user: #{article['user']['id']}
#{article['body']}
BODY_MD
wip: false,
message: '[skip notice] Imported from Qiita',
user: users_map[article['user']['id']] || 'esa_bot', # 記事作成者上書き: owner権限が必要
}
if dry_run
puts "***** index: #{index} *****"
pp params
puts
next
end
print "[#{Time.now}] index[#{index}] #{article['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)
require 'esa'
require 'json'
require 'pp'
require 'pry'
class Importer
def initialize(client, wiki_repo_url)
@client = client
`git clone #{wiki_repo_url} wiki_data` unless File.exist? 'wiki_data'
Dir.chdir('wiki_data')
@file_names = Dir.glob('*.md')
end
attr_accessor :client, :file_names
def wait_for(seconds)
(seconds / 10).times do
print '.'
sleep 10
end
puts
end
def import(dry_run: true)
file_names.each do |file_name|
title = File.basename(file_name)
params = {
name: title,
category: "Imports/GitHubWiki",
body_md: File.read(file_name),
wip: false,
message: '[skip notice] Imported from GitHub Wiki',
user: 'esa_bot' # 記事作成者上書き: owner権限が必要
}
if dry_run
pp params
puts
next
end
print "[#{Time.now}] #{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, 'https://github.com/[organization]/[repo].wiki.git')
# dry_run: trueで確認後に dry_run: falseで実際にimportを実行
importer.import(dry_run: true)
# はじめに この記事では esa API v1について説明します。 # 概要 ## リクエスト 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`をリクエストヘッダに追加して下さい。文字コードはUTF-8を使用して下さい。 ## エラーレスポンス エラーが発生した場合は、エラーを表現するオブジェクトを含んだエラーレスポンスが返却されます。 ```json { "error": "not_found", "message": "Not found" } ``` ## ページネーション リストを返すAPIでは、すべての要素を一度に返すことは現実的ではないため、ページを指定することができるようになっています。これらのAPIには、1から始まるページ番号を表す `page` パラメータと、1ページあたりに含まれる要素数を表す `per_page` パラメータを指定することができます。`page` の初期値は `1` に設定されています。また、 `per_page` の初期値は `20`、最大値は `100` に設定されています。 ページを指定できるAPIでは、ページネーションの情報を含んだレスポンスを返します。 ``` # GET /v1/teams HTTP/1.1 { "teams": [ ... ], "prev_page": null, "next_page": 2, "total_count": 30, "page": 1, "per_page": 20, "max_per_page": 100 } ``` - prev_page - 1つ前のpage番号。存在しない場合はnull - next_page - 1つ先のpage番号。存在しない場合はnull - total_count - リソースの総数 - page - 現在のページ番号 - per_page - 1ページあたりに含まれる要素数 - max_per_page - per_pageに指定可能な数の最大値 ## クライアントライブラリ 現時点で以下のライブラリが利用可能です。 - Ruby - [esaio/esa-ruby](https://github.com/esaio/esa-ruby) # 認証と認可 esa APIを利用するには、アクセストークンをリクエストに含める必要があります。アクセストークンは、ユーザの管理画面(https://[team].esa.io/user/tokens)もしくはOAuthを利用した認可フローにより発行できます。 ## アクセストークン アクセストークンは、以下のようにリクエストヘッダに含められます。 ``` Authorization: Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ``` また、URIクエリ文字列として指定することも可能です。 ``` api.esa.io/v1/teams?access_token=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ``` ## スコープ 個々のアクセストークンには、複数のスコープを紐付けることができます。アクセストークンが適切なスコープを持っている時のみAPIを利用することができます。esa APIでは以下のスコープを利用することができます。 | スコープ | 説明 | | --- | --- | | read | データの読み出し | | write | データの書き込み | ## OAuthを利用した認可フロー ### アプリケーションの登録 `https://[team].esa.io/user/applications` から アプリケーションを登録できます。 ※ RailsやSinatraでは、以下の手順は [omniauth-esa](https://github.com/esaio/omniauth-esa) を使うことで実装の手間を省くことが出来ます。 ### GET /oauth/authorize アプリケーションのユーザーに認可画面を表示します。 - client_id - 登録されたアプリケーションのclient_idです - redirect_uri - 認可が行われた後のリダイレクト先を指定します。アプリケーション登録時に設定したredirect_uriのうちいずれかに一致する必要があります。 - `urn:ietf:wg:oauth:2.0:oob`を指定した場合、認可後にリダイレクトせず画面に認証コードを表示します - scope - アプリケーションが利用するスコープをスペース区切りで指定します - response_type - `code` を指定します - state - CSRF対策のため、認可後にリダイレクトするURLのクエリに含まれる値を指定できます ``` GET /oauth/authorize?client_id=0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&scope=read+write&state=a7e567e2fb858f0e12838798016ee9cf8ccc778 HTTP/1.1 ``` <img width="578" alt="ss 2016-04-21 14.49.33.png (138.0 kB)" src="https://img.esa.io/uploads/production/attachments/105/2016/04/21/1/781dc446-c038-49aa-81f0-7486d9a4e265.png"> ### POST /oauth/token 新規にアクセストークンを発行します。 - client_id - 登録されたアプリケーションのClient IDです - client_secret - 登録されたアプリケーションのClient Secretです - grant_type - `authorization_code ` を指定します - redirect_uri - 認可を行った際のredirect_uriを指定します - code - 認可後のリダイレクト先のURLに含まれた認可コードです ``` POST /oauth/token HTTP/1.1 {"client_id":"0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04","client_secret":"ca6eb9452f2fdaaa68a8d870bc654db8e6f466f6d42685b914e04c442b2065a2","code":"c486e26492dfdc42bcf4e07bfbd84a97f98548c1cf89178a512e76b79b099313","grant_type":"authorization_code","redirect_uri":"urn:ietf:wg:oauth:2.0:oob"} ``` ``` HTTP/1.1 200 OK { "access_token": "5d49aab1f0796bbbd78100f06c6cd4d667851644012d37421073bb61126cdafc", "token_type": "bearer", "scope": "read write", "created_at": 1461218696 } ``` ### GET /oauth/token/info アクセストークンの情報を取得します ``` GET /oauth/token/info HTTP/1.1 Authorization: bearer 5d49aab1f0796bbbd78100f06c6cd4d667851644012d37421073bb61126cdafc ``` ``` HTTP/1.1 200 OK {"resource_owner_id":1,"scopes":["read","write"],"expires_in_seconds":null,"application":{"uid":"0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04"},"created_at":1461218696} ``` ### POST /oauth/revoke アクセストークンを失効させます - client_id - 登録されたアプリケーションのClient IDです - client_secret - 登録されたアプリケーションのClient Secretです - token - アクセストークンを指定します ``` POST /oauth/revoke HTTP/1.1 {"client_id":"0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04","client_secret":"ca6eb9452f2fdaaa68a8d870bc654db8e6f466f6d42685b914e04c442b2065a2","token":"5d49aab1f0796bbbd78100f06c6cd4d667851644012d37421073bb61126cdafc"} ``` ``` HTTP/1.1 200 OK {} ``` # チーム 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 /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, "page": 1, "per_page": 20, "max_per_page": 100 } ``` ## GET /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/" } ``` # 統計情報 チームの統計情報を表します。 - members(Integer) - チーム内のメンバーの総数です - posts(Integer) - チーム内の記事の総数です - posts_wip(Integer) - チーム内の記事(wip)の総数です - posts_shipped(Integer) - チーム内の記事(shipped)の総数です - comments(Integer) - チーム内の記事につけられたコメントの総数です - stars(Integer) - チーム内の記事につけられたスターの総数です - daily_active_users(Integer) - 過去24時間以内に記事の新規投稿/更新・コメント・Star等の活動を行ったメンバー数です。 - weekly_active_users(Integer) - 過去7日間以内にに記事の新規投稿/更新・コメント・Star等の活動を行ったメンバー数です。 - monthly_active_users(Integer) - 過去30日間以内にに記事の新規投稿/更新・コメント・Star等の活動を行ったメンバー数です。 ## GET /v1/teams/:team_name/stats ``` GET /v1/teams/foo/stats HTTP/1.1 ``` ``` { "members": 20, "posts": 1959, "posts_wip": 59, "posts_shipped": 1900, "comments": 2695, "stars": 3115, "daily_active_users": 8, "weekly_active_users": 14, "monthly_active_users": 15 } ``` # メンバー チームのメンバーを表します。 - name (String) - メンバーの名前です - screen_name (String) - メンバーのスクリーンネームです - icon (String) - メンバーのアイコンのURLです - email (String) - メンバーのemailです - このフィールドは **team の owner** だけが取得可能です - posts_count (Integer) - チーム内でメンバーが作成した記事数です ## GET /v1/teams/:team_name/members ``` GET /v1/teams/foo/members HTTP/1.1 ``` ``` { "members": [ { "name": "Atsuo Fukaya", "screen_name": "fukayatsu", "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png", "email": "fukayatsu@esa.io", "posts_count": 222 }, { "name": "TAEKO AKATSUKA", "screen_name": "taea", "icon": "https://img.esa.io/uploads/production/users/2/icon/thumb_m_2690997f07b7de3014a36d90827603d6.jpg", "email": "taea@esa.io", "posts_count": 111 } ], "prev_page": null, "next_page": null, "total_count": 2, "page": 1, "per_page": 20, "max_per_page": 100 } ``` # 記事 ユーザが作成した記事を表します。 - 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 /v1/teams/:team_name/posts 記事の一覧を更新日の降順で返却します ``` GET /v1/teams/docs/posts HTTP/1.1 ``` ### URIクエリ文字列 - q (String) - 記事を絞り込むための条件を指定します - [#104: help/記事の検索方法](/posts/104) を参照 - include (String) - `comments` を指定するとコメントの配列を含んだレスポンスを返します。 - `stargazers` を指定するとStarの配列を含んだレスポンスを返します。 - `stargazers,comments` のように `,` で区切ることで複数指定できます - engine (String) - `v2` を指定すると、新しい記事検索エンジンがご利用可能です - [#198: ReleaseNotes/2016/11/19/記事検索を高速化し、インクリメンタルサーチができるようになりました](/posts/198) - このパラメータは一時的な措置で、2016/12/14以降はv2がデフォルトになります - sort (String) - engine=v2のときに使用可能です。記事のソート方法を指定します。 - 設定可能な値 - `updated` (default) - 記事の更新日時 - `created` - 記事の作成日時 - `stars` - 記事へのStarの数 - `watches` - 記事へのWatchの数 - `comments` - 記事へのCommentの数 - `best_match` - 総合的な記事のスコア - order (String) - engine=v2のときに使用可能です。記事ソートの方向を指定します。 - 設定可能な値 - `desc` (default) - 降順 - `asc` - 昇順 ``` { "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, "page": 1, "per_page": 20, "max_per_page": 100 } ``` ## GET /v1/teams/:team_name/posts/:post_number 指定された投稿を取得します。 ### URIクエリ文字列 - include (String) - `comments` を指定するとコメントの配列を含んだレスポンスを返します。 ``` 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/: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 /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 /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 /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" }, "stargazers_count": 0, "star": false } ], "prev_page": null, "next_page": null, "total_count": 1, "page": 1, "per_page": 20, "max_per_page": 100 } ``` ## GET /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" }, "stargazers_count": 0, "star": false } ``` ## POST /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" }, "stargazers_count": 0, "star": false } ``` ## PATCH /v1/teams/:team_name/comments/:comment_id 指定されたコメントを更新します。 - comment (Object) - body_md (String) - user(String) - チームメンバーのscreen_nameもしくは "esa_bot" を指定することで記事の投稿者を上書きすることができます。 - このパラメータは **team の owner** だけ が使用することができます。 ``` 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" }, "stargazers_count": 0, "star": false } ``` ## DELETE /v1/teams/:team_name/comments/:comment_id 指定されたコメントを削除します。 ``` DELETE /v1/teams/docs/comments/22767 HTTP/1.1 ``` ``` HTTP/1.1 204 No Content ``` # Star - created_at (DateTime) - Starをした日時です。 - body (null or String) - 引用Starの本文です。 - user - Starをしたユーザです。 ## GET /v1/teams/:team_name/posts/:post_number/stargazers 指定された記事にStarをしたユーザ一覧を取得します。 ``` GET /v1/teams/kama/posts/2312/stargazers HTTP/1.1 ``` ``` HTTP/1.1 200 OK Content-Type: application/json { "stargazers": [ { "created_at": "2016-05-05T11:40:54+09:00", "body": null, "user": { "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, "page": 1, "per_page": 20, "max_per_page": 100 } ``` ## POST /v1/teams/:team_name/posts/:post_number/star 指定された記事にStarをします。 - body (String) - (任意) 引用Starの本文です。 ``` POST /v1/teams/kama/posts/2312/star HTTP/1.1 {"body":"foo bar"} ``` ``` HTTP/1.1 204 No Content ``` ## DELETE /v1/teams/:team_name/posts/:post_number/star 指定された記事へのStarを取り消します。 ``` DELETE /v1/teams/kama/posts/2312/star HTTP/1.1 ``` ``` HTTP/1.1 204 No Content ``` ## GET /v1/teams/:team_name/comments/:comment_id/stargazers 指定されたコメントにStarをしたユーザ一覧を取得します。 ``` GET /v1/teams/kama/comments/123/stargazers HTTP/1.1 ``` ``` HTTP/1.1 200 OK Content-Type: application/json { "stargazers": [ { "created_at": "2016-05-05T11:40:54+09:00", "body": null, "user": { "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, "page": 1, "per_page": 20, "max_per_page": 100 } ``` ## POST /v1/teams/:team_name/comments/:comment_id/star 指定されたコメントにStarをします。 - body (String) - (任意) 引用Starの本文です。 ``` POST /v1/teams/kama/comments/123/star HTTP/1.1 {"body":"foo bar"} ``` ``` HTTP/1.1 204 No Content ``` ## DELETE /v1/teams/:team_name/comments/:comment_id/star 指定されたコメントへのStarを取り消します。 ``` DELETE /v1/teams/kama/comments/123/star HTTP/1.1 ``` ``` HTTP/1.1 204 No Content ``` # Watch - created_at (DateTime) - Watchをした日時です。 - user - Watchをしたユーザです。 ## GET /v1/teams/:team_name/posts/:post_number/watchers 指定された記事にWatchをしたユーザ一覧を取得します。 ``` GET /v1/teams/kama/posts/2312/watchers HTTP/1.1 ``` ``` HTTP/1.1 200 OK Content-Type: application/json { "watchers": [ { "created_at": "2016-05-05T11:40:53+09:00", "user": { "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, "page": 1, "per_page": 20, "max_per_page": 100 } ``` ## POST /v1/teams/:team_name/posts/:post_number/watch 指定された記事にWatchをします。 ``` POST /v1/teams/kama/posts/2312/watch HTTP/1.1 ``` ``` HTTP/1.1 204 No Content ``` ## DELETE /v1/teams/:team_name/posts/:post_number/watch 指定された記事のWatchを取り消します。 ``` DELETE /v1/teams/kama/posts/2312/watch HTTP/1.1 ``` ``` HTTP/1.1 204 No Content ``` # 認証中のユーザ ## GET /v1/user 現在のアクセストークンで認証中のユーザの情報を表します。 - id (Integer) - サービス内で一意なユーザIDです - name (String) - ユーザの名前です - screen_name (String) - ユーザのスクリーンネームです - created_at (DateTime String) - ユーザの作成日時です - updated_at (DateTime String) - ユーザの更新日時です - icon (String) - ユーザのアイコンのURLです - email (String) - ユーザのemailアドレスです ### URIクエリ文字列 - include (String) - `teams` を指定すると所属するチームの配列を含んだレスポンスを返します。 ``` GET /v1/user HTTP/1.1 Content-Type: application/json ``` ``` HTTP/1.1 200 OK Content-Type: application/json { "id": 1, "name": "Atsuo Fukaya", "screen_name": "fukayatsu", "created_at": "2014-05-10T11:50:07+09:00", "updated_at": "2016-04-17T12:35:16+09:00", "icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png", "email": "fukayatsu@esa.io" } ``` # 他サービスからのインポートスクリプトのサンプル ## Qiita:Team ```import.rb require 'esa' require 'json' require 'pp' class Importer def initialize(client, file_path) @client = client @exported_data = JSON.parse(File.read(file_path)) return if @exported_data['version'] == 'v1' puts "This script is for Qiita Export Format v1. Please contact us (hi@esa.io) (\\( ⁰⊖⁰)/)" exit 1 end attr_accessor :client, :exported_data 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', } exported_data['articles'].sort_by{ |article| article['updated_at'] }.each.with_index do |article, index| next unless index >= start_index params = { name: article['title'], category: "Imports/Qiita", tags: article['tags'].map{ |tag| tag['name'].gsub('/', '-') }, body_md: <<-BODY_MD, created_at: #{article['created_at']} user: #{article['user']['id']} #{article['body']} BODY_MD wip: false, message: '[skip notice] Imported from Qiita', user: users_map[article['user']['id']] || 'esa_bot', # 記事作成者上書き: owner権限が必要 } if dry_run puts "***** index: #{index} *****" pp params puts next end print "[#{Time.now}] index[#{index}] #{article['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) ``` ## GitHub Wiki ```ruby:import.rb require 'esa' require 'json' require 'pp' require 'pry' class Importer def initialize(client, wiki_repo_url) @client = client `git clone #{wiki_repo_url} wiki_data` unless File.exist? 'wiki_data' Dir.chdir('wiki_data') @file_names = Dir.glob('*.md') end attr_accessor :client, :file_names def wait_for(seconds) (seconds / 10).times do print '.' sleep 10 end puts end def import(dry_run: true) file_names.each do |file_name| title = File.basename(file_name) params = { name: title, category: "Imports/GitHubWiki", body_md: File.read(file_name), wip: false, message: '[skip notice] Imported from GitHub Wiki', user: 'esa_bot' # 記事作成者上書き: owner権限が必要 } if dry_run pp params puts next end print "[#{Time.now}] #{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, 'https://github.com/[organization]/[repo].wiki.git') # dry_run: trueで確認後に dry_run: falseで実際にimportを実行 importer.import(dry_run: true) ``` # 今後の実装予定 - Preview API - Revision API
この記事では esa API v1について説明します。
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
をリクエストヘッダに追加して下さい。文字コードはUTF-8を使用して下さい。
エラーが発生した場合は、エラーを表現するオブジェクトを含んだエラーレスポンスが返却されます。
{
"error": "not_found",
"message": "Not found"
}
リストを返すAPIでは、すべての要素を一度に返すことは現実的ではないため、ページを指定することができるようになっています。これらのAPIには、1から始まるページ番号を表す page
パラメータと、1ページあたりに含まれる要素数を表す per_page
パラメータを指定することができます。page
の初期値は 1
に設定されています。また、 per_page
の初期値は 20
、最大値は 100
に設定されています。
ページを指定できるAPIでは、ページネーションの情報を含んだレスポンスを返します。
# GET /v1/teams HTTP/1.1
{
"teams": [
...
],
"prev_page": null,
"next_page": 2,
"total_count": 30,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
現時点で以下のライブラリが利用可能です。
esa APIを利用するには、アクセストークンをリクエストに含める必要があります。アクセストークンは、ユーザの管理画面(https://[team].esa.io/user/tokens)もしくはOAuthを利用した認可フローにより発行できます。
アクセストークンは、以下のようにリクエストヘッダに含められます。
Authorization: Bearer 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
また、URIクエリ文字列として指定することも可能です。
api.esa.io/v1/teams?access_token=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
個々のアクセストークンには、複数のスコープを紐付けることができます。アクセストークンが適切なスコープを持っている時のみAPIを利用することができます。esa APIでは以下のスコープを利用することができます。
スコープ | 説明 |
---|---|
read | データの読み出し |
write | データの書き込み |
https://[team].esa.io/user/applications
から アプリケーションを登録できます。
※ RailsやSinatraでは、以下の手順は omniauth-esa を使うことで実装の手間を省くことが出来ます。
アプリケーションのユーザーに認可画面を表示します。
urn:ietf:wg:oauth:2.0:oob
を指定した場合、認可後にリダイレクトせず画面に認証コードを表示しますcode
を指定しますGET /oauth/authorize?client_id=0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&scope=read+write&state=a7e567e2fb858f0e12838798016ee9cf8ccc778 HTTP/1.1
新規にアクセストークンを発行します。
authorization_code
を指定しますPOST /oauth/token HTTP/1.1
{"client_id":"0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04","client_secret":"ca6eb9452f2fdaaa68a8d870bc654db8e6f466f6d42685b914e04c442b2065a2","code":"c486e26492dfdc42bcf4e07bfbd84a97f98548c1cf89178a512e76b79b099313","grant_type":"authorization_code","redirect_uri":"urn:ietf:wg:oauth:2.0:oob"}
HTTP/1.1 200 OK
{
"access_token": "5d49aab1f0796bbbd78100f06c6cd4d667851644012d37421073bb61126cdafc",
"token_type": "bearer",
"scope": "read write",
"created_at": 1461218696
}
アクセストークンの情報を取得します
GET /oauth/token/info HTTP/1.1
Authorization: bearer 5d49aab1f0796bbbd78100f06c6cd4d667851644012d37421073bb61126cdafc
HTTP/1.1 200 OK
{"resource_owner_id":1,"scopes":["read","write"],"expires_in_seconds":null,"application":{"uid":"0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04"},"created_at":1461218696}
アクセストークンを失効させます
POST /oauth/revoke HTTP/1.1
{"client_id":"0a9bff757caf1eeb344421211637bb6cf724da767062069cafe16a43283a6e04","client_secret":"ca6eb9452f2fdaaa68a8d870bc654db8e6f466f6d42685b914e04c442b2065a2","token":"5d49aab1f0796bbbd78100f06c6cd4d667851644012d37421073bb61126cdafc"}
HTTP/1.1 200 OK
{}
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,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
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/"
}
チームの統計情報を表します。
GET /v1/teams/foo/stats HTTP/1.1
{
"members": 20,
"posts": 1959,
"posts_wip": 59,
"posts_shipped": 1900,
"comments": 2695,
"stars": 3115,
"daily_active_users": 8,
"weekly_active_users": 14,
"monthly_active_users": 15
}
チームのメンバーを表します。
GET /v1/teams/foo/members HTTP/1.1
{
"members": [
{
"name": "Atsuo Fukaya",
"screen_name": "fukayatsu",
"icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png",
"email": "fukayatsu@esa.io",
"posts_count": 222
},
{
"name": "TAEKO AKATSUKA",
"screen_name": "taea",
"icon": "https://img.esa.io/uploads/production/users/2/icon/thumb_m_2690997f07b7de3014a36d90827603d6.jpg",
"email": "taea@esa.io",
"posts_count": 111
}
],
"prev_page": null,
"next_page": null,
"total_count": 2,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
ユーザが作成した記事を表します。
<h1>Getting Started</h1>
created_by
と共通です。記事の一覧を更新日の降順で返却します
GET /v1/teams/docs/posts HTTP/1.1
comments
を指定するとコメントの配列を含んだレスポンスを返します。stargazers
を指定するとStarの配列を含んだレスポンスを返します。stargazers,comments
のように ,
で区切ることで複数指定できますv2
を指定すると、新しい記事検索エンジンがご利用可能です
updated
(default)
created
stars
watches
comments
best_match
desc
(default)
asc
{
"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,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
指定された投稿を取得します。
comments
を指定するとコメントの配列を含んだレスポンスを返します。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"
},
"stargazers_count": 0,
"star": false
}
],
"prev_page": null,
"next_page": null,
"total_count": 1,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
指定されたコメントを取得します。
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"
},
"stargazers_count": 0,
"star": false
}
記事に新しいコメントを作成します。
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"
},
"stargazers_count": 0,
"star": false
}
指定されたコメントを更新します。
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"
},
"stargazers_count": 0,
"star": false
}
指定されたコメントを削除します。
DELETE /v1/teams/docs/comments/22767 HTTP/1.1
HTTP/1.1 204 No Content
指定された記事にStarをしたユーザ一覧を取得します。
GET /v1/teams/kama/posts/2312/stargazers HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"stargazers": [
{
"created_at": "2016-05-05T11:40:54+09:00",
"body": null,
"user": {
"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,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
指定された記事にStarをします。
POST /v1/teams/kama/posts/2312/star HTTP/1.1
{"body":"foo bar"}
HTTP/1.1 204 No Content
指定された記事へのStarを取り消します。
DELETE /v1/teams/kama/posts/2312/star HTTP/1.1
HTTP/1.1 204 No Content
指定されたコメントにStarをしたユーザ一覧を取得します。
GET /v1/teams/kama/comments/123/stargazers HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"stargazers": [
{
"created_at": "2016-05-05T11:40:54+09:00",
"body": null,
"user": {
"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,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
指定されたコメントにStarをします。
POST /v1/teams/kama/comments/123/star HTTP/1.1
{"body":"foo bar"}
HTTP/1.1 204 No Content
指定されたコメントへのStarを取り消します。
DELETE /v1/teams/kama/comments/123/star HTTP/1.1
HTTP/1.1 204 No Content
指定された記事にWatchをしたユーザ一覧を取得します。
GET /v1/teams/kama/posts/2312/watchers HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"watchers": [
{
"created_at": "2016-05-05T11:40:53+09:00",
"user": {
"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,
"page": 1,
"per_page": 20,
"max_per_page": 100
}
指定された記事にWatchをします。
POST /v1/teams/kama/posts/2312/watch HTTP/1.1
HTTP/1.1 204 No Content
指定された記事のWatchを取り消します。
DELETE /v1/teams/kama/posts/2312/watch HTTP/1.1
HTTP/1.1 204 No Content
現在のアクセストークンで認証中のユーザの情報を表します。
teams
を指定すると所属するチームの配列を含んだレスポンスを返します。GET /v1/user HTTP/1.1
Content-Type: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1,
"name": "Atsuo Fukaya",
"screen_name": "fukayatsu",
"created_at": "2014-05-10T11:50:07+09:00",
"updated_at": "2016-04-17T12:35:16+09:00",
"icon": "https://img.esa.io/uploads/production/users/1/icon/thumb_m_402685a258cf2a33c1d6c13a89adec92.png",
"email": "fukayatsu@esa.io"
}
require 'esa'
require 'json'
require 'pp'
class Importer
def initialize(client, file_path)
@client = client
@exported_data = JSON.parse(File.read(file_path))
return if @exported_data['version'] == 'v1'
puts "This script is for Qiita Export Format v1. Please contact us (hi@esa.io) (\\( ⁰⊖⁰)/)"
exit 1
end
attr_accessor :client, :exported_data
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',
}
exported_data['articles'].sort_by{ |article| article['updated_at'] }.each.with_index do |article, index|
next unless index >= start_index
params = {
name: article['title'],
category: "Imports/Qiita",
tags: article['tags'].map{ |tag| tag['name'].gsub('/', '-') },
body_md: <<-BODY_MD,
created_at: #{article['created_at']}
user: #{article['user']['id']}
#{article['body']}
BODY_MD
wip: false,
message: '[skip notice] Imported from Qiita',
user: users_map[article['user']['id']] || 'esa_bot', # 記事作成者上書き: owner権限が必要
}
if dry_run
puts "***** index: #{index} *****"
pp params
puts
next
end
print "[#{Time.now}] index[#{index}] #{article['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)
require 'esa'
require 'json'
require 'pp'
require 'pry'
class Importer
def initialize(client, wiki_repo_url)
@client = client
`git clone #{wiki_repo_url} wiki_data` unless File.exist? 'wiki_data'
Dir.chdir('wiki_data')
@file_names = Dir.glob('*.md')
end
attr_accessor :client, :file_names
def wait_for(seconds)
(seconds / 10).times do
print '.'
sleep 10
end
puts
end
def import(dry_run: true)
file_names.each do |file_name|
title = File.basename(file_name)
params = {
name: title,
category: "Imports/GitHubWiki",
body_md: File.read(file_name),
wip: false,
message: '[skip notice] Imported from GitHub Wiki',
user: 'esa_bot' # 記事作成者上書き: owner権限が必要
}
if dry_run
pp params
puts
next
end
print "[#{Time.now}] #{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, 'https://github.com/[organization]/[repo].wiki.git')
# dry_run: trueで確認後に dry_run: falseで実際にimportを実行
importer.import(dry_run: true)