Skip to content

Instantly share code, notes, and snippets.

@yano3nora
Last active October 15, 2019 08:44
Show Gist options
  • Save yano3nora/663a1137a1a443b2d9db08204f917361 to your computer and use it in GitHub Desktop.
Save yano3nora/663a1137a1a443b2d9db08204f917361 to your computer and use it in GitHub Desktop.
[dev: Instagram Graph API] Instagram Graph API / Facebook Graph API. #dev #sns

OVERVIEW

Instagram Graph API

Facebook 提供の製品で、Facebook アカウントと連携している Instagram ビジネスアカウント向けの WEB API サービス。Instagram が Facebook 傘下になったことからか、旧 Instagram API は 2020 年までに廃止されることとなり Instagram Graph API として Facebook の製品群に取り込まれる形になった。利用するためにはアプリケーションレビュー ( = 審査 ) が必要で、Sandbox 的な グラフ API エクスプローラ でアプリを開発/テストしレビューを経て認可を受けなければならない。

Refs


FACEBOOK GRAPH API x INSTAGRAM

Process

  1. Facebook ページ を作成
  2. 上記 Facebook ページへ Instagram ビジネスアカウントを紐づけ
    • なければ新規作成
  3. アプリを登録
    • アプリ ID
    • app secret キーを取得しておく
  4. アプリダッシュボードから Facebook ログイン を追加
  5. アプリダッシュボードから Instagram API を追加
    • アプリレビューから申請予定のアクセス権限を追加
  6. グラフ API エクスプローラを使用してアプリ動作テスト
    • 自身が管理する Facebook ページへのアクセス権限 manage_pages を選択
      • show_pages_list でもページリストは取得可能だが ビジネスマネージャ経由でページでの役割が付与されている場合manage_pages がないとダメ ( 参考 )
    • インスタ系は instagram_basic の他申請予定のものを選択
  7. アプリのアプリレビューを申請する
    • スクリーンキャストは こんな感じ でよいみたい
    • ダイレクトサポートへの問い合わせで審査が進展することも ありました

API Access Token

Facebook API: 有効期限の長い Access Token を取得する

アクセストークンには以下 3 種類あり、それぞれ有効期限がある。

  • App アクセストークン
    • ${APP_ID}|${APP_SECRET} の形でアプリケーションの設定などの権限を持つ
    • APP_SECRET を渡すため server to serve call 前提
    • アプリケーションの作成時に生成され、開発側で値を変更しなければ変わらないので実質無期限
    • 基本的に各ユーザカウントの個人情報には触れない
      • 公開されているノード ( 写真とか ) の ID から公開情報は取れる
      • ユーザアカウントノード ID からユーザの保持情報 ... みたいなことはできない
  • User アクセストークン
    • 乱数で生成され有効期限は 1 時間程度の短命トークン
      • アプリにトークンの延長登録して 2 ヶ月程度の長期トークンを生成できる
    • 当該ユーザがアプリに許可した各種個人情報へアクセスできる
    • 無期限化や自分以外のユーザアカウント参照などは当然できない
  • Page アクセストークン
    • 短命または長期 User アクセストークンをクエリに含んで /me/accounts をコールすると生成される
      • 短命トークンで作った Page アクセストークンは短命に
      • 長期トークンで作った Page アクセストークンは無期限になる ( 正常な動作なのか不明 )
    • User アクセストークンに付与された権限を引き継ぎ、当該ユーザの「ページ」関連情報へアクセスできる
      • ページ連携の Instagram アカウントもこれでアクセス可能

API Endpoints

API は REST 形式で「ノード /${NODE_ID}/ 」に対して GET / POST / DELETE メソッドでリクエストしていく感じ。

/debug_token

/debug_token

$ 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

/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"
}

/me

/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/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/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"
    }
  }
}

/instagram-user/media

/user /media Instagram ( ビジネス ) アカウント配下メディアリソースへアクセス

# 要求可能なフィールドなどはグラフ 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"
    }
  ]
}

TIPS & REFERENCES

Instagram embedding API

Embedding - instagram.com/developer

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.jsinstgrm.Embeds.process() をぶっ叩けばよいみたい。多分内部的に現在 DOM ツリー上の Instagram oEmbed なエレメントを探索して iframe 生成しているぽい。

いろいろな oEmbed 埋め込みコンテンツを遅延ロードしてみる

<!-- Instagram が利用している oEmbed 用スクリプトを読み込み -->
<script src="https://www.instagram.com/embed.js" defer></script>
// React などで動的に oEmbed な <blockquotes> が DOM ツリーに追加された後に ...
window.instgrm.Embeds.process();

レスポンシブ対応

InstagramのEmbedコードをレスポンシブに対応させる。

どこまでも世話の焼けるヤツだ 標準だとピクセル固定になっているのでリサイズ時にレスポンシぶらないといけない。面倒い。

.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;
    }
  }
}

リダイレクトが発生する API

Facebook Graph API - Received Invalid JSON reply

/me/picture などリダイレクトが発生するエンドポイントがいくつかあるので Ajax の際は ?redirect=false を付ける

無期限の Page アクセストークン

# まずは 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}"

JavaScript SDK

FB.api

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'));
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment