Facebook 提供の製品で、Facebook アカウントと連携している Instagram ビジネスアカウント向けの WEB API サービス。Instagram が Facebook 傘下になったことからか、旧 Instagram API は 2020 年までに廃止されることとなり Instagram Graph API として Facebook の製品群に取り込まれる形になった。利用するためにはアプリケーションレビュー ( = 審査 ) が必要で、Sandbox 的な グラフ API エクスプローラ でアプリを開発/テストしレビューを経て認可を受けなければならない。
- 非エンジニア向けInstagramAPI解説
- nstagram Graph API で Instagram に画像を投稿してみた
- Instagram API廃止に備えてInstagram Graph API(グラフAPI)へ移行
- Facebook ページ を作成
- 上記 Facebook ページへ Instagram ビジネスアカウントを紐づけ
- なければ新規作成
- アプリを登録
- アプリ ID
- app secret キーを取得しておく
- アプリダッシュボードから
Facebook ログイン
を追加 - アプリダッシュボードから
Instagram API
を追加- アプリレビューから申請予定のアクセス権限を追加
- グラフ API エクスプローラを使用してアプリ動作テスト
- 自身が管理する Facebook ページへのアクセス権限
manage_pages
を選択show_pages_list
でもページリストは取得可能だが ビジネスマネージャ経由でページでの役割が付与されている場合 はmanage_pages
がないとダメ ( 参考 )
- インスタ系は
instagram_basic
の他申請予定のものを選択
- 自身が管理する Facebook ページへのアクセス権限
- アプリのアプリレビューを申請する
アクセストークンには以下 3 種類あり、それぞれ有効期限がある。
- App アクセストークン
${APP_ID}|${APP_SECRET}
の形でアプリケーションの設定などの権限を持つ- APP_SECRET を渡すため server to serve call 前提
- アプリケーションの作成時に生成され、開発側で値を変更しなければ変わらないので実質無期限
- 基本的に各ユーザカウントの個人情報には触れない
- 公開されているノード ( 写真とか ) の ID から公開情報は取れる
- ユーザアカウントノード ID からユーザの保持情報 ... みたいなことはできない
- User アクセストークン
- 乱数で生成され有効期限は 1 時間程度の短命トークン
- アプリにトークンの延長登録して 2 ヶ月程度の長期トークンを生成できる
- 当該ユーザがアプリに許可した各種個人情報へアクセスできる
- 無期限化や自分以外のユーザアカウント参照などは当然できない
- 乱数で生成され有効期限は 1 時間程度の短命トークン
- Page アクセストークン
- 短命または長期 User アクセストークンをクエリに含んで
/me/accounts
をコールすると生成される- 短命トークンで作った Page アクセストークンは短命に
- 長期トークンで作った Page アクセストークンは無期限になる ( 正常な動作なのか不明 )
- User アクセストークンに付与された権限を引き継ぎ、当該ユーザの「ページ」関連情報へアクセスできる
- ページ連携の Instagram アカウントもこれでアクセス可能
- 短命または長期 User アクセストークンをクエリに含んで
API は REST 形式で「ノード
/${NODE_ID}/
」に対して GET / POST / DELETE メソッドでリクエストしていく感じ。
$ curl -X GET "debug_token?input_token=${ACCESS_TOKEN}"
{
"data": {
"app_id": "xxxxxxxxxxxxxxxx",
"type": "USER",
"application": "xxxxxxx",
"data_access_expires_at": 0,
"expires_at": 0,
"is_valid": true,
"issued_at": 1543046841,
"profile_id": "xxxxxxxxxxxxxx",
"scopes": [
"email",
"manage_pages",
"instagram_basic",
"public_profile"
],
"user_id": "xxxxxxxxxxxxxx"
}
}
/oauth/access_token
利用者がFacebookログインを使用してアプリに接続し、アクセス許可のリクエストを承認すると、そのアプリはアクセストークンを取得できます。これにより、アプリは一時的に、セキュアな方法でFacebook APIを利用できます。
$ curl -X GET "https://graph.facebook.com/oauth/access_token
> ?grant_type=fb_exchange_token&client_id=${APP_ID}&client_secret=${APP_SECRET}
> &fb_exchange_token=${SHORT_USER_ACCESS_TOKEN}"
{
"access_token": "xxxxxx", # 延長されたユーザアクセストークン ( 2 ヶ月 )
"token_type": "bearer"
}
/user のエイリアス、アクセストークンから Facebook ユーザノードを引きあて
$ curl -X GET "https://graph.facebook.com/me?access_token=${USER_ACCESS_TOKEN}"
{
"name": "Your Facebook Name",
"id": "xxxxxxxxxxxxxx" # Facebook ユーザノード ID
}
/user/picture user のプロフィール画像を取得、
?width=
を適当な値で投げるとそれに近い大きさの画像を返してくれる。ちなみに?redirect=false
で画像へのリダイレクトが抑止され画像データの JSON が返る
$ curl -X GET "https://graph.facebook.com/me/picture?width=700" # 画像へリダイレクト
$ curl -X GET "https://graph.facebook.com/me/picture?redirect=false" # 画像データ JSON が返る
/user/accounts user 配下の各種アカウント ( Pages とか ) リソースへアクセス
$ curl -X GET "https://graph.facebook.com/me/accounts
> ?fields=cover,instagram_business_account{username,ig_id}
> &access_token=${USER_ACCESS_TOKEN__OR__PAGE_ACCESS_TOKEN}"
{
"data": [
{
"access_token": "xxxxxxxxxxx", # Page アクセストークン ( 延長済み User トークンを投げれば無期限のものが返る )
"cover": {
"cover_id": "xxxxxxxxxx",
"offset_x": 50,
"offset_y": 57,
"source": "https://xxxxx" # ページのカバー写真ソース
},
"instagram_business_account": {
"id": "xxxxxxxxxxxxxxxxxxx", # Instagram ビジネスアカウント ID
"ig_id": "xxxxxxxxxxxxxxxx", # Instagram ユーザ ID
"username": "xxxxxxxxxxxxx" # Instagram ユーザネーム
},
"id": "xxxxxxxxxxxxxxxxxxx" # Facebook ページノード ID
}
],
"paging": {
"cursors": {
"before": "xxxxxxx",
"after": "xxxxxxx"
}
}
}
# 要求可能なフィールドなどはグラフ API エクスプローラの画面右より設定
$ curl -X GET "https://graph.facebook.com/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/media
> ?fields=caption,media_type,media_url,timestamp,permalink,comments&limit=1
> &access_token=${USER_ACCESS_TOKEN__OR__PAGE_ACCESS_TOKEN}
> "
{
"data": [
{
"caption": "Media caption text",
"media_type": "IMAGE",
"media_url": "https://xxxxxxx",
"timestamp": "2018-11-16T08:05:37+0000",
"permalink": "https://www.instagram.com/p/xxxxxx/",
"id": "xxxxxx"
}
]
}
Instagram の公開 API で画像の oEmbed 情報や画像の CDN リンクが取得可能、以下両方ともリダイレクトが発生するので注意。
# oEmbed 用の HTML ソースが取得可能
# &maxwidth=320 以上の指定で大きさを多少調整できる
# &hidecaption=false でキャプションを消せる
$ curl -X GET "https://api.instagram.com/oembed?url=http://instagr.am/p/fA9uwTtkSN/"
# CDN 上の JPG へリダイレクトされる
# ?size= に t thumbnail, m medium, l large が指定可能 ( デフォルト m )
$ curl -X GET "https://instagram.com/p/fA9uwTtkSN/media/?size=t"
また oEmbed されたウィジェットは React などで動的に生成された場合中身がロードされない。公式曰く embed.js
で instgrm.Embeds.process()
をぶっ叩けばよいみたい。多分内部的に現在 DOM ツリー上の Instagram oEmbed なエレメントを探索して iframe 生成しているぽい。
<!-- Instagram が利用している oEmbed 用スクリプトを読み込み -->
<script src="https://www.instagram.com/embed.js" defer></script>
// React などで動的に oEmbed な <blockquotes> が DOM ツリーに追加された後に ...
window.instgrm.Embeds.process();
どこまでも世話の焼けるヤツだ 標準だとピクセル固定になっているのでリサイズ時にレスポンシぶらないといけない。面倒い。
.instagram-oembed-wrapper {
display: flex;
justify-content: center; // 真ん中よせ
.instagram-oembed { // こいつの下に <blockquote> を設置
width: 100%;
@media (max-width: 393px) {
// 最小値 320px なので設置時のページ余白分はみでるのを調整
transform: scale(0.8);
width: auto;
}
iframe.instagram-media.instagram-media-rendered {
max-width: 100% !important;
}
}
}
/me/picture
などリダイレクトが発生するエンドポイントがいくつかあるので Ajax の際は ?redirect=false
を付ける
# まずは OAuth して User アクセストークン ( 短命 ) を取得
# ここはユーザに能動的にアプリを許可してログインしてもらう他ない
# 手動でやる場合は https://graph.facebook.com/v2.5/dialog/oauth みたいな API をキックしてコールバックをもらうみたい
# 長期 User アクセストークンをリクエスト
# APP_SECRET を投げるので Server to Server call 必須
$ curl -X GET "https://graph.facebook.com/oauth/access_token
> ?grant_type=fb_exchange_token&client_id=${APP_ID}&client_secret=${APP_SECRET}
> &fb_exchange_token=${SHORT_USER_ACCESS_TOKEN}"
# 返却された長期 User アクセストークンで無期限 Page トークンを取得
$ curl -X GET "https://graph.facebook.com/me/accounts
> ?access_token=${LONG_USER_ACCESS_TOKEN}"
# 返却された Page トークンをアクセストークンデバッガで確認
$ curl -X GET "debug_token?input_token=${PAGE_ACCESS_TOKEN}"
Facebook ログイン → アバター画像の URL を DB 格納 ... みたいな感じ。中身全然精査してないけどとりあえず動いたコードをはっておく ... 。
<fb:login-button
size="small"
button-type="continue_with"
use-continue-as="true"
onlogin="FB.avatar()"
scope="public_profile,manage_pages,instagram_basic"
></fb:login-button>
document.addEventListener('DOMContentLoaded', () => {
window.fbAsyncInit = function() {
FB.init({
appId : document.documentElement.dataset.facebookAppId,
cookie : true,
xfbml : true,
version : 'v3.2'
});
/**
* Ensure login.
* @param void
* @return Promise (resolve: authResponse, reject: response)
*/
FB.ensureLogin = () => {
return new Promise((resolve, reject) => {
FB.getLoginStatus((response) => {
if (response.status === 'connected') {
return resolve(response.authResponse)
}
FB.login((response) => {
if (!response.authResponse) {
reject(response)
}
resolve(response.authResponse)
})
})
})
}
/**
* Get facebook avatar into user.avatar.
* @param void
* @return window.location.reload()
*/
FB.avatar = () => {
FB.ensureLogin()
.then((auth) => {
return new Promise((resolve, reject) => {
FB.api(`/me/picture?width=700`,
{
access_token: auth.accessToken,
redirect: false
},
(response) => {
if (!response.data) reject(response)
resolve({uid: auth.userID, avatar: response.data.url})
})
})
})
.then((user) => {
return new Promise((resolve, reject) => {
request.patch(`${location.origin}/brands/profile/avatar`)
.set('Content-Type', 'application/json')
.set('X-Requested-With', 'XMLHttpRequest')
.set('X-CSRF-Token', document.querySelector('meta[name="csrf-token"]').content)
.send({
user: {
uid: user.uid,
avatar: user.avatar,
provider: 'facebook',
}
})
.end((err, res) => {
if (err) reject(err)
resolve(res)
})
})
})
.then((results) => {
console.log(results)
window.location.reload()
})
.catch((err) => {
console.error(err)
})
}
/**
* Get instagram resource into brand.resource.
* @param void
* @return window.location.reload()
*/
FB.instagram = (username) => {
FB.ensureLogin()
.then((auth) => {
return new Promise((resolve, reject) => {
if (!username) return reject({body: {message: 'Process aborted.'}})
FB.api(`/me/accounts?fields=cover,instagram_business_account{username}`,
{
access_token: auth.accessToken,
redirect: false
},
(response) => {
if (!response.data) return reject(response)
let page = response.data
.filter((page) => page.hasOwnProperty('instagram_business_account'))
.find((page) => page.instagram_business_account.username == username)
if (!page) {
return reject({body: {message: 'Instagram account was not found.'}})
}
return resolve({
pageId: page.id,
brand: {
logo: page.cover.source,
resource: {
resource_user_id: page.instagram_business_account.id,
resource_username: page.instagram_business_account.username,
resource_access_token: auth.accessToken,
}
}
})
})
})
})
.then((params) => {
return new Promise((resolve, reject) => {
request.put(`${location.origin}/brands/resource/instagram/${params.pageId}`)
.set('Content-Type', 'application/json')
.set('X-Requested-With', 'XMLHttpRequest')
.set('X-CSRF-Token', document.querySelector('meta[name="csrf-token"]').content)
.send({brand: params.brand})
.end((err, res) => {
if (err) return reject(err)
if (!res.body.status) return reject(res)
return resolve(res)
})
})
})
.then((results) => {
if (results.body) alert(results.body.message)
console.log(results)
window.location.reload()
})
.catch((err) => {
if (err.body) alert(err.body.message)
console.error(err)
})
}
/**
* Detach facebook accounts.
* @param string resource
* @return window.location.reload()
*/
FB.detach = (resource) => {
try {
if (!confirm(`Detach ${resource} account?`)) {
return console.warn('Detach aborted.')
}
console.log(`${resource} detach isn't implemented yet.`)
} catch (e) {
console.error(e)
}
}
};
let locale = document.documentElement.lang
switch (locale) {
case 'ja':
locale = 'ja_JP'
break
default:
locale = 'en_US'
}
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = `https://connect.facebook.net/${locale}/sdk.js#xfbml=1&version=v3.2`;
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
})