facebookのタイムラインへpythonから投稿する

  • 14
    Like
  • 0
    Comment
More than 1 year has passed since last update.

自分自身やfacebookページのタイムラインへ、pythonから投稿してみよう。

残念なことにfacebookにはincoming webhookの仕組みがないため、投稿用のfacebookアプリを作らなければならない。

またfacebookを操作するにはアクセストークンが必要だが、facebookはOAuth認証が必須なため、CLIからアクセストークンを取得することは難しい(headlessなブラウザを使いレスポンスを解析すれば不可能ではないが)。

そこで今回は、localhost上にpythonでCGIサーバを起動し、OAuthのコールバックをCGIで処理してアクセストークンを取得することにする。

facebook groupへも投稿できるようにしたかったが、そのためにはfacebook運営者によるfacebookアプリの承認が必要なようで、残念ながら今回は断念。

検証した環境

version
OS Ubuntu 15.04 (3.19.0-26-generic)
Python 2.7.10
pip 7.1.2
requests 2.8.1
jinja2 2.8
facebook-sdk 0.4.0
facebook graph API 2.5

ソースコード

https://github.com/nmatsui/post_facebook_using_python

facebookアプリの作成

まずfacebookアプリを作成する。

1 facebookの開発者ポータル https://developers.facebook.com/ から「Add A New App」を表示し、「ウェブサイト」を選択する

fb_python_01.png

2 作成するfacebookアプリの名前(今回は python_publisher )を入力して「Create New Facebook App ID」を押す

fb_python_02.png

3 カテゴリを選択して(今回は ユーティリティ )「アプリIDを作成」を押す

fb_python_03.png

4 OAuthでの認証コールバック用のURL(今回は http://localhost:8000/cgi-bin/get_token )を入力して「Next」を押す

fb_python_04.png

5 facebookアプリの作成完了

fb_python_05.png

6 開発者ポータルのトップページを再度表示し、「My Apps」から作成したアプリを選択する

作成したアプリが見当たらない場合、開発者ポータルをリロードする

fb_python_06.png

7 App IDとApp Secretを確認する

App Secretを確認する際に、パスワードの入力を求められる場合がある

fb_python_07.png

設定ファイルの作成

作成したfacebookアプリの情報や、投稿するfacebookページの情報を記載した設定ファイルを作成する。CGIプログラムの都合上、設定ファイル名は conf.json で固定。

conf.json
{
  "app": {
    "app_id": "facebookアプリのApp ID",
    "app_secret": "facebookアプリのApp Secret",
    "redirect_uri": "facebookアプリに設定したコールバックURL"
  },
  "page": {
    "name": "投稿するfacebookページの名前"
  }
}

環境準備

facebookアプリができたので、次はpython環境を準備する。

ライブラリのインストール

conf.jsonと同じ場所にrequirements.txtを作成し、以下3つのライブラリをpipを用いてインストールする。

  • requests
    • facebookのREST APIにアクセスする
  • jinja2
    • HTMLを生成するためのテンプレートエンジン
  • facebook-SDK
    • pythonからfacebookに投稿するためのFacebook Graph APIラッパー
requirements.txt
requests
jinja2
facebook-sdk
pipでインストール
$ pip install -r requirements.txt

CGI用ディレクトリの作成

conf.jsonを配置したディレクトリに、CGIプログラムを置く cgi-bin ディレクトリとjinja2テンプレートを置く templates ディレクトリを作成する

ディレクトリ作成
$ mkdir cgi-bin
$ mkdir templates

CGIプログラムの作成

今回は次の二つのCGIを作成する

  • index
    • facebookアプリのOAuth認証URLを生成してリンクを表示する
  • get_token
    • facebookからコールバックされる
    • タイムラインへ投稿するためのアクセストークンを生成する

アクセストークンに与える権限

今回は、以下3つの権限範囲を指定する。指定できる権限の詳細はPermissions Reference - Facebook Loginを参照。

  • manage_pages
    • facebookページを管理する権限
    • (facebookページのリストアップやaccess tokenの取得に用いる)
  • publish_pages
    • facebookページに投稿する権限
  • publish_actions
    • 投稿に関する権限
    • (facebookアプリが未承認の状態では、投稿できるのは自らのタイムラインのみ)

アクセストークンにはデフォルトで public_profile 権限(公開プロフィールを取得する権限)が与えられるため、今回取得するアクセストークンはこれら4つの権限範囲内での操作が許可される。

index

facebookアプリのOAuth認証URLを生成し、ブラウザにそのリンクを表示する。

OAuth認証URL生成CGI

cgi-bin/index は、conf.jsonを読み込み下記フォーマットのOAuth認証URLを生成する。

https://www.facebook.com/dialog/oauth?redirect_uri=<facebookアプリに設定したコールバックURL&client_id=<facebookアプリのApp ID>&scope=<承認を与える権限>

cgi-bin/index
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import json
import urllib
from jinja2 import Environment, FileSystemLoader

CONF_FILE = 'conf.json'
BASE_URL = 'https://www.facebook.com/dialog/oauth'
SCOPE = 'manage_pages,publish_actions,publish_pages'
TPL_DIR = './templates'
TEMPLATE = 'index.tpl.html'


def create_url():
    with open(CONF_FILE, 'r') as f:
        conf = json.load(f)
    redirect_uri = urllib.quote_plus(conf['app']['redirect_uri'])
    url = BASE_URL + '?'
    url += 'redirect_uri=' + redirect_uri + '&'
    url += 'client_id=' + conf['app']['app_id'] + '&'
    url += 'scope=' + SCOPE
    return url


def main():
    params = {}
    try:
        url = create_url()
        params['isOK'] = True
        params['url'] = url
    except Exception as e:
        params['isOK'] = False
        params['error_type'] = type(e).__name__
        params['error_title'] = str(e)

    env = Environment(loader=FileSystemLoader(TPL_DIR, encoding='utf-8'))
    tpl = env.get_template(TEMPLATE)
    html = tpl.render(params)

    print('Content-type: text/html')
    print('\n')
    print(html.encode('utf-8'))

main()

コールバックURLはそのままだとURLパラメータとして渡せないので、urllib.quote_plus()でエスケープしている。

OAuth認証URL表示テンプレート

templates/index.tpl.html は、生成したURLをリンクとして表示する。

templates/index.tpl.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>index</title>
  </head>
  <body>
  {% if isOK %}
    <a href="{{ url }}">get token</a>
  {% else %}
    <b>次のエラーが発生しました</b><br/>
    [{{ error_type }}] {{ error_title }}
  {% endif %}
  </body>
</html>

get_token

facebookのOAuth認証機能からコールバックされ、認証コードを取得する。認証コードを用いてfacebookのAPIから以下のアクセストークンを取得し、jsonファイルとして保存する。

  • User Access Token
    • 自分自身(アプリ開発者)のアクセストークン
  • Page Access Token
    • 設定ファイルで指定したfacebook pageのアクセストークン

アクセストークン取得CGI

cgi-bin/get_token は、コールバックされた認証コードを用いてUser Access Tokenと指定されたfacebookページのPage Access Tokenを取得する。
取得したアクセストークンは、token.jsonに保存する。

cgi-bin/get_token
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cgi
import json
import re
import requests
from jinja2 import Environment, FileSystemLoader

CONF_FILE = 'conf.json'
TOKEN_FILE = 'token.json'
TOKEN_URL = 'https://graph.facebook.com/oauth/access_token'
ACCOUNT_URL = 'https://graph.facebook.com/me/accounts'
USER_ACCESS_TOKEN_PATTERN = r'access_token=([^&=]+)(&expires=\d+)?'
TPL_DIR = './templates'
TEMPLATE = 'get_token.tpl.html'


class TokenRetriever(object):

    def __init__(self, code):
        self.code = code
        with open(CONF_FILE, 'r') as f:
            self.conf = json.load(f)

    def get_token(self):
        user_access_token = self.__get_user_access_token()
        page_access_token = self.__get_page_access_token(user_access_token)
        token = {}
        token['user_access'] = user_access_token
        token['page_access'] = page_access_token
        token_json = json.dumps({'token': token}, indent=2, sort_keys=True)
        return token_json

    def __get_user_access_token(self):
        payload = {}
        payload['client_id'] = self.conf['app']['app_id']
        payload['client_secret'] = self.conf['app']['app_secret']
        payload['redirect_uri'] = self.conf['app']['redirect_uri']
        payload['code'] = self.code
        response = requests.get(TOKEN_URL, params=payload)
        m = re.match(USER_ACCESS_TOKEN_PATTERN, response.text)
        if m:
            return self.__exchange_token(m.group(1))
        else:
            raise LookupError('access_token does not exist')

    def __get_page_access_token(self, user_access_token):
        payload = {}
        payload['access_token'] = user_access_token
        response = requests.get(ACCOUNT_URL, params=payload)
        pages = filter(lambda p: p['name'] == self.conf['page']['name'],
                       json.loads(response.text)['data'])
        page_access_token = pages[0]['access_token']
        return self.__exchange_token(page_access_token)

    def __exchange_token(self, token):
        payload = {}
        payload['client_id'] = self.conf['app']['app_id']
        payload['client_secret'] = self.conf['app']['app_secret']
        payload['grant_type'] = 'fb_exchange_token'
        payload['fb_exchange_token'] = token
        response = requests.get(TOKEN_URL, params=payload)
        m = re.match(USER_ACCESS_TOKEN_PATTERN, response.text)
        if m:
            return m.group(1)
        else:
            raise LookupError('access_token does not exist')


def main():
    params = {}

    try:
        form = cgi.FieldStorage()
        if not form.has_key('code'):
            raise LookupError('QueryString "code" does not exist')

        token_retriever = TokenRetriever(form['code'].value)
        token_json = token_retriever.get_token()
        with open(TOKEN_FILE, 'w') as f:
            f.write(token_json)

        params['isOK'] = True
        params['token_file'] = TOKEN_FILE
        params['token_json'] = token_json
    except Exception as e:
        params['isOK'] = False
        params['error_type'] = type(e).__name__
        params['error_title'] = str(e)

    env = Environment(loader=FileSystemLoader(TPL_DIR, encoding='utf-8'))
    tpl = env.get_template(TEMPLATE)
    html = tpl.render(params)

    print('Content-type: text/html; charset=utf-8')
    print('\n')
    print(html.encode('utf-8'))

main()

facebookのREST APIを利用するために、requestsライブラリを利用している。なおfacebookのUser Access Tokenは、通常では取得後1〜2時間で有効期限が切れる。それではテストが面倒だったので、アクセストークン延長REST APIを用いて60日有効なトークンに交換している。

アクセストークン表示テンプレート

templates/get_token.tpl.html は、取得したアクセストークンを表示する。

templates/get_token.tpl.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>get_token</title>
  </head>
  <body>
  {% if isOK %}
    <b>User Access Token</b><b>Page Access Token</b>を取得しました。</br>
    トークンは次のJSONフォーマットで{{ token_file }}に書き出しました。
    <pre>{{ token_json }}</pre>
  {% else %}
    <b>次のエラーが発生しました。</b><br/>
    [{{ error_type }}] {{ error_title }}
  {% endif %}
  </body>
</html>

CGIプログラムの動作とアクセストークン取得

では、CGIを動作させてアクセストークンを取得する。今回は検証なので、nginxやapache等のhttpdは用いずにpython2.7のCGIHTTPServerで代用する。

CGIHTTPServer起動

conf.jsonやcgi-binディレクトリ、templatesディレクトリがあるディレクトリから、以下のコマンドでCGIHTTPServerを起動する。

CGIHTTPServer起動
$ python -m CGIHTTPServer

indexへアクセス

CGIHTTPServerを起動したPCのブラウザから、次のURLへアクセスする。

http://localhost:8000/cgi-bin/index

fb_python_08.png

OAuthで認証される

get_tokenをクリックするとfacebookアプリの認証が行われ、権限の承認を問われる。

1 ユーザに与える権限のチェック
承認する権限のチェックを行う。このfacebookアプリは未承認のため、グループへの書き込みなど権限の一部が無効化されている旨の警告が表示される。

fb_python_09.png

2 投稿範囲のチェック
facebookアプリが自分に成り代わって投稿する公開範囲を指定する。

fb_python_10.png

3 facebookページへの権限チェック
facebookページの管理権限と投稿権限のチェックを行う。facebookページへの投稿に関しては、未承認アプリでも警告は出ない。

fb_python_11.png

アクセストークンの取得と表示

CGIがアクセストークンを取得してtoken.jsonに保存し、画面へも表示する。

fb_python_12.png

token.json
{
  "token": {
    "page_access": "取得したPage Access Token", 
    "user_access": "取得したUser Acdess Token"
  }
}

facebookへの投稿

アクセストークンが取得できたので、pythonからfacebookへ投稿する。facebookのREST APIを直接操作しても良いが、今回はFacebook SDK for Pythonを使う。

pythonスクリプト

SDKとアクセストークンを用いて自分自身とfacebookページのEndpointを取得し、メッセージを書き込む。自分のタイムラインとfacebookページのタイムラインでは、書き込みメソッドが異なることに注意。

post.py
#!/usr/bin/env python
# -*- encode: utf-8 -*-

import sys
import json
import facebook


class Timeline:

    def __init__(self, token_file):
        with open(token_file, 'r') as f:
            token = json.load(f)['token']
            self.user_endpoint = facebook.GraphAPI(token['user_access'])
            self.page_endpoint = facebook.GraphAPI(token['page_access'])

    def post_me(self, msg):
        self.user_endpoint.put_object('me', 'feed', message=msg)
        print('posted to my timeline: %s' % msg)

    def post_page(self, msg):
        self.page_endpoint.put_wall_post(message=msg)
        print('posted to page timeline: %s' % msg)

if __name__ == '__main__':
    if len(sys.argv) != 4:
        print('usage: %s token_file [me|page] message' % sys.argv[0])
        exit(1)
    try:
        timeline = Timeline(sys.argv[1])
        if sys.argv[2] == 'me':
            timeline.post_me(sys.argv[3])
        elif sys.argv[2] == 'page':
            timeline.post_page(sys.argv[3])
        else:
            print '%s is invalid' % sys.argv[2]
    except (IOError, facebook.GraphAPIError) as e:
        print e
        exit(9)

自分のタイムラインへの投稿

自分のタイムラインへ投稿してみる。

自分のタイムラインへの投稿
$ ./post.py token.json me "テスト投稿。これはpythonスクリプトからの投稿のテストです。"
posted to my timeline: テスト投稿。これはpythonスクリプトからの投稿のテストです。

facebook_python_12.png

facebookページのタイムラインへの投稿

今回の実験用に作成したfacebookページのタイムラインへ投稿してみる。

$ ./post.py token.json page "テスト投稿。これはpythonスクリプトからの投稿のテストです。"
posted to page timeline: テスト投稿。これはpythonスクリプトからの投稿のテストです。

fb_python_13.png

最後に

ただpythonからfacebookへ投稿したかっただけなのに、非常に長い道のりだったが、自分のタイムラインやfacebookページのタイムラインへ無事に投稿することができた。facebookもincoming webhookを準備してくれないかな?