モブ沢工房

プログラミングとかLinux関連(特にOSSのグラフィックツール関連)とかレトロゲームとか3Dプリンタやら日曜大工等、色々。

Python@oauth2ではてなブログのAtompubを使って大投稿

引っ越しスクリプトの為に頑張りました。 というか、WSSEを使ったほうが楽っぽかったんですが、ついつい。

そして、非常に苦労しました… まずoauthの仕組みがよく分からずに始めたので、

  1. アプリ登録でconsumer keyとconsumer secret keyを得る。
  2. consumer keyとconsumer secret keyでoauth2.Consumerオブジェクトを作り、そこからさらに実作業に用いるoauth2.Clientオブジェクトを作成する。
  3. リクエストトークンを得る。(リクエストトークンは認証の為のものでもあるので、取得のたびに変化する。)
  4. リクエストトークンをセットして認証ページを開き、ユーザーにリクエストしたことを認証させ、認証キーを得させる。
  5. 制限時間内(はてなでは5分以内)に、アクセストークンのページにリクエストトークンと認証キーを送り、アクセストークンを得る
  6. 得られたアクセストークンを継続的に使ってAPIを使用する。

という手順がよく理解できませんでした。

oauth2.Consumer->oauth2.Clientを作るのに、このような関数を作りました。

def create_client(token=None): 
    cons=oauth2.Consumer(CONSUMER_KEY,CONSUMER_SECRET_KEY)  
    if token:  
        return oauth2.Client(cons,token)  
    return oauth2.Client(cons)  

そして、認証用スクリプトではこのように、ざくっと。

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

#import time,random,urllib,cgi,hmac,hashlib
import oauth2
import urlparse
import urllib
import subprocess
import os
import pickle
import sys

from baseinfo import *


def get_request_token(scope,fname):
    if os.path.exists(fname):
        os.unlink(fname)
    client=create_client()

    params={}
    params['scope']=scope
    params['oauth_callback']='oob' # assign to display oauth_verifier

    resp, content=client.request("%s?%s" % (REQUEST_TOKEN_URL , urllib.urlencode(params)))
    if resp['status'] != '200':
        print("ERROR STATUS:%s" % resp['status'])
        return

    request_token=dict(urlparse.parse_qsl(content))

    print("*** SUCESSFULLY ACQUIRE REQUEST TOKEN ***")
    print(" you still need to verify your request. call this program with verifier key.")

    print("Request token:")
    print("token : %s" % request_token['oauth_token'])
    print("token secret : %s" % request_token['oauth_token_secret'])

    with open(fname,'w') as ofp:
        ofp.write(pickle.dumps(request_token))

    subprocess.call(('firefox','%s?oauth_token=%s' % (REDIRECT_URL,request_token['oauth_token'])))

def get_access_token(verifier,reqfname,acfname):
    print("[GETTING ACCESS KEY]")
    request_token=None
    with open(reqfname,'r') as ifp:
        request_token=pickle.loads(ifp.read())

    token=oauth2.Token(request_token['oauth_token'],request_token['oauth_token_secret'])
    token.set_verifier(verifier)
    client=create_client(token)

    resp, content=client.request(ACCESS_TOKEN_URL)

    print content
    if resp['status'] != '200':
        print("ERROR STATUS:%s" % resp['status'])
        os.unlink(reqfname)
        return

    access_token=dict(urlparse.parse_qsl(content))

    print("Access token:")
    for ck in access_token:
        print("%s:%s" % (ck,access_token[ck]))

    # adding request tokens to access tokens
    for ck in request_token:
        access_token["request_%s" % ck]=request_token[ck]

    with open(acfname,'w') as ofp:
        ofp.write(pickle.dumps(access_token))

    os.unlink(reqfname)

    print("*** SUCESSFULLY ACQUIRE ACCESS TOKEN ***")
    print("you can use HATENA api with oauth.")
    print("all needed key writed at the file %s , with python.pickle format." % acfname)
    print("you can use these keys as dictionary object from pickle.loads() method")
    


if __name__ == '__main__':

    if len(sys.argv) > 1:
        if sys.argv[1] in ('clear','init','clean'):
            try:
                os.unlink(TOKEN_INFO)
            except OSError:
                pass
            try:
                os.unlink(ACCESS_TOKEN_FILE)
            except OSError:
                pass
        else:
            get_access_token(sys.argv[1],TOKEN_INFO,ACCESS_TOKEN_FILE)
            sys.exit(0)

    get_request_token("read_public,write_public,read_private,write_private",TOKEN_INFO)

baseinfoモジュールでは自分用のアプリケーションキー秘密鍵、URL、そして先ほどのcreate_client()などを置いてあります。全部大文字の定数はそこで定義されてるものです。

なんだかリダイレクトが難しそうだったのでサクッとfirefoxを開いて手動で認証し、手動で認証キーをコピーし、ターミナルにペーストして認証用スクリプトを起動するとアクセストークンを取得するという、原始的な仕組みになっております。引数の処理もgetoptなんか使わずにいい加減ですw

これでACCESS_TOKEN_FILEに秘密鍵などごっそり書かれるので本当はchmod og-rwxでもしといたほうがいいのでしょうが、とりあえずこれで…

この後、GETメソッドでブログエントリの一覧とかを得るのは普通に出来たのですが、苦労したのがPOSTでの投稿でした。

長くなるので貼りませんが、clientオブジェクトを持続的に使うため、クラス化して各APIメソッドで実装して行こうと考えてやっていったのですが、 (xmlは最近慣れてきたpython-jinja2でテンプレートで処理することにしました)

        resp, content=self.client.request("%s/entry" % self.baseURL,'POST',body.encode(self.CHARSET))

こうして投稿すると400 XML parse errorが帰ってきます。

しばらく考えた後、そうか!ヘッダがxml指定されてないんだ!と思いまして

        header={}
        header['content-type']='application/xml'
        resp, content=self.client.request("%s/entry" % self.baseURL,'POST',body.encode(self.CHARSET),header)

みたいにすると、今度は認証に失敗します…

さんざググった挙句、ドキュメントやサンプルが少なすぎてわけがわからなくなったので、ソースを見ることにしました。これまたどこにソースがあるのかよくわからない…

結局ipythonなどでいじっている間に、ubuntu 14.04では/usr/lib/python2.7/dist-packages/oauth2/init.pyがそれであることがわかり読んでみました。

どうも、Content-Typeがヘッダに書かれているときは、それだけ抜き出して既存のヘッダに書き加えてくれるようです… なぜ動かぬ?と思ってよぉーくみると

content-type

小文字ですた!!

これで比較に失敗してヘッダ自体を入れ替えていたっぽい…当然、トークン無しの状況に…

        header={}
        header['Content-Type']='application/xml'
        resp, content=self.client.request("%s/entry" % self.baseURL,'POST',body.encode(self.CHARSET),header)

こうすることで動くようになりました。結局headerもいきなり入れちゃっていい感じになりましたが…途中、oauth_signatureとか全部手動生成も視野に入れる状況でしたんで、こうなっています。 まぁそのうち治すと言う感じで。

ちなみにxmlのupdatedタグ部分には過去の日付を設定できるので、これなら引っ越しも安心ですね。

これでどうも引っ越しスクリプトへの野望も峠を越えた感が。

まぁ、ググって見つけたサイトでfc2からはてなブログへは、はてなダイアリー経由で簡単に引っ越せるという情報を教えていただいたのですが、画像は持ってこれないっぽいのと、はてなダイアリーはdullhiko時代にやっていて引っ越しのためにまたアカウント取るのもな〜とか思って、こんな回り道をしています。