====== Google App Engineで手軽にOAuthアプリを作成!(Twitterとか!) - AppEngine-OAuth ======
更新履歴とコメントは[[http://0-oo.net/log/category/python-box/google-app-engine-twitter/|AppEngine-Twitter Archive - ゼロと無限の間のログ]]へどうぞ。
{{:javascript:09_s.jpg|}}
サイトに対してパスワードを預けないで済むという画期的なプロトコルOAuth。仕様はややこしいようで簡単、なようで意外に複雑。\\
日本語情報が少ないのが痛い。:-\
でも1回作ってしまえば使い回しが効く。ということでライブラリ化した。
今のところTwitter APIでの使用しか考えていないが、他のサービスでもある程度使えるんじゃないかなー。
===== ライセンス =====
[[http://0-oo.net/pryn/MIT_license.txt|MITライセンス]]で。
===== デモ(動作サンプル) =====
==== その1. RT(ReTweet)の多いつぶやきを報告するBot ====
[[http://twitter.com/RT_report|@RT_report]]
==== その2. 地名を告げるとその近くにいるTwitterユーザーの名前を教えてくれるBot ====
[[http://twitter.com/who_is_near|@who_is_near]]
=== 使用例 ===
@who_is_near 六本木
試しにつぶやくには、Twitterにログイン後に[[http://twitter.com/?status=%40who_is_near%20%E5%85%AD%E6%9C%AC%E6%9C%A8|ここをクリック]]
==== その3. 出発地点と目的地を告げると、乗換(ルート)案内のGoogleマップを表示してくれるBot ====
[[http://twitter.com/norikae_map|@norikae_map]]
=== 使用例 1. 移動方法を指定しないと、電車でのルート検索・乗り換え案内になる ===
@norikae_map 東京 大阪
試しにつぶやくには、Twitterにログイン後に[[http://twitter.com/?status=%40norikae_map%20%E6%9D%B1%E4%BA%AC%20%E5%A4%A7%E9%98%AA|ここをクリック]]
=== 使用例 2. 車でのルートの案内 ===
@norikae_map 木更津駅 羽田空港駅 車
試しにつぶやくには、Twitterにログイン後に[[http://twitter.com/?status=%40norikae_map%20%E6%9C%A8%E6%9B%B4%E6%B4%A5%E9%A7%85%20%E7%BE%BD%E7%94%B0%E7%A9%BA%E6%B8%AF%E9%A7%85%20%E8%BB%8A|ここをクリック]]
=== 使用例 3. 歩きでのルートの案内 ===
@norikae_map 霞が関 霞が関 歩き
試しにつぶやくには、Twitterにログイン後に[[http://twitter.com/?status=%40norikae_map%20%E9%9C%9E%E3%81%8C%E9%96%A2%20%E9%9C%9E%E3%81%8C%E9%96%A2%20%E6%AD%A9%E3%81%8D|ここをクリック]]
==== その4. TwitterにOAuthでログインしてTwitter APIを使うデモ ====
[[http://0-oo.appspot.com/oauth/|TwitterにOAuthでログインしてTwitter APIを使うデモ]]
ソースコード\\
(※[[python-box/appengine-basehandler|AppEngine Basehandler]]と[[python-box/appengine-twitter|AppEngine Twitter]]を使っている)
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
'''
Sample for AppEngine-OAuth on Google App Engine
See: http://0-oo.net/sbox/python-box/appengine-oauth
See also: http://apiwiki.twitter.com/OAuth-FAQ
'''
import json
import logging
import webapp2
from appengine_twitter import AppEngineTwitter
from basehandler import BaseHandler, h
from google.appengine.ext import db
OAUTH_KEY = 'xxxxx'
OAUTH_SECRET = 'xxxxx'
class DemoHandler(BaseHandler):
def demo_header(self):
self.simple_header(u'AppEngine-OAuthのデモ')
self.p(u'AppEngine-OAuthのデモ
')
def demo_footer(self):
self.p('
')
self.p('
', True)
self.p(u'このページは')
self.p('')
self.p(u'AppEngine-OAuth と ')
self.p('')
self.p(u'AppEngine-Twitter のデモです')
self.simple_footer()
class InitHandler(DemoHandler):
def get(self):
twitter = AppEngineTwitter()
twitter.set_oauth(key=OAUTH_KEY, secret=OAUTH_SECRET)
req_info = twitter.prepare_oauth_login()
# request_tokenはcallbackされた後で使うのでDatastoreに保存しておく
OAuthRequestToken(token=req_info['oauth_token'],
secret=req_info['oauth_token_secret']).put()
self.demo_header()
self.p('')
self.p(u'TwitterのOAuthログインへ', True)
self.demo_footer()
class CallbackHandler(DemoHandler):
def get(self):
twitter = AppEngineTwitter()
twitter.set_oauth(OAUTH_KEY, OAUTH_SECRET)
# TwitterからHTTP GETでrequest_tokenが渡される
req_token = self.request.get('oauth_token')
# Datastoreに保存しておいたreqest_token_secretを取り出す
query = OAuthRequestToken.all()
query.filter('token = ', req_token)
req_tokens = query.fetch(1)
# request_tokenとaccess_tokenを交換する
acc_token = twitter.exchange_oauth_tokens(req_token, req_tokens[0].secret)
# ここまで来ればOAuthを使ってAPIが使える。試しにユーザー名を取得
twitter.verify()
name = json.loads(twitter.last_response.content)['screen_name']
self.demo_header()
self.p(u'こんにちは、' + name + u'さん!', True)
self.p('
')
# 以後は再ログインせずにaccess_tokenを繰り返し使うことができる
tw2 = AppEngineTwitter()
tw2.set_oauth(OAUTH_KEY,
OAUTH_SECRET,
acc_token['oauth_token'],
acc_token['oauth_token_secret'])
msg = u'AppEngine-OAuth (on Python and Google App Engine) '
msg += u'http://0-oo.appspot.com/oauth/ からこんにちは!'
tw2.update(msg.encode('utf8'))
self.p(u'つぶやいたよ(自分のTLを見てね)', True)
self.p('
')
if tw2.is_following('uresuji_books') == False:
tw2.follow('uresuji_books')
self.p(u'@uresuji_books をフォローしたよ', True)
self.p('
')
self.p(u'最初のページへ戻る', True)
self.demo_footer()
#Model(s)
class OAuthRequestToken(db.Model):
token = db.StringProperty()
secret = db.StringProperty()
logging.getLogger()
routing = [('/oauth/', InitHandler),
('/oauth/callback', CallbackHandler)]
application = webapp2.WSGIApplication(routing, debug=False)
===== AppEngine-OAuthのソースコード =====
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
AppEngine-OAuth
OAuth utility for applications on Google App Engine
See: http://0-oo.net/sbox/python-box/appengine-oauth
License: http://0-oo.net/pryn/MIT_license.txt (The MIT license)
'''
__author__ = 'dgbadmin@gmail.com'
__version__ = '0.1.1'
import hmac
import urllib
from google.appengine.api import urlfetch
from hashlib import sha1
from random import getrandbits
from time import time
class AppEngineOAuth(object):
def __init__(self, key, secret, acs_token='', acs_token_secret=''):
self._key = key
self._secret = secret
self._token = acs_token
self._token_secret = acs_token_secret
# Be understandable which type token is (request or access)
if acs_token == '':
self._token_type = None
else:
self._token_type = 'access'
def prepare_login(self, req_token_url):
'''
Return request_token, request_token_secret and params of authorize url.
'''
# Get request token
params = self.get_oauth_params(req_token_url, {})
res = urlfetch.fetch(url=req_token_url + '?' + urllib.urlencode(params),
method='GET')
self.last_response = res
if res.status_code != 200:
raise Exception('OAuth Request Token Error: ' + res.content)
# Response content is request_token
dic = self._qs2dict(res.content)
self._token = dic['oauth_token']
self._token_secret = dic['oauth_token_secret']
self._token_type = 'request'
# Get params with signature
sig_params = {'oauth_signature': params['oauth_signature']}
dic['params'] = urllib.urlencode(self.get_oauth_params(req_token_url,
sig_params))
return dic
def exchange_tokens(self, acs_token_url, req_token, req_token_secret):
self._token = req_token
self._token_secret = req_token_secret
self._token_type = 'request'
params = urllib.urlencode(self.get_oauth_params(acs_token_url, {}))
res = urlfetch.fetch(url=acs_token_url, payload=params, method='POST')
self.last_response = res
if res.status_code != 200:
raise Exception('OAuth Access Token Error: ' + res.content)
# Response content is access_token
dic = self._qs2dict(res.content)
self._token = dic['oauth_token']
self._token_secret = dic['oauth_token_secret']
self._token_type = 'access'
return dic
def get_oauth_params(self, url, params, method='GET'):
oauth_params = {'oauth_consumer_key': self._key,
'oauth_signature_method': 'HMAC-SHA1',
'oauth_timestamp': int(time()),
'oauth_nonce': getrandbits(64),
'oauth_version': '1.0'}
if self._token_type != None:
oauth_params['oauth_token'] = self._token
# Add other params
params.update(oauth_params)
# Sort and concat
s = ''
for k in sorted(params):
s += self._quote(k) + '=' + self._quote(params[k]) + '&'
msg = method + '&' + self._quote(url) + '&' + self._quote(s[:-1])
# Maybe token_secret is empty
key = self._secret + '&' + self._token_secret
digest = hmac.new(key, msg, sha1).digest()
params['oauth_signature'] = digest.encode('base64')[:-1]
return params
def _quote(self, s):
return urllib.quote(str(s), '')
def _qs2dict(self, s):
dic = {}
for param in s.split('&'):
(key, value) = param.split('=')
dic[key] = value
return dic