Web APIに柔軟に対応するために
MetaGatewayは,他のブログエディタではサポートしていないさまざまなサービスなどを,すべて同じように操作することができます。具体的には「対応サービスとサポートする機能」を参照してください。
さらにこれらのサービスだけでなく,Google Mapや各種アフィリエイト,その他さまざまなAPIを利用して,外部サイトとのマッシュアップを行っています。数だけでいえば,ある意味究極のマッシュアップサービスかもしれません。
MetaGatewayでは新しいサービス等が増えても,非常に簡単に機能追加が可能になるように注意して設計されています。APIや認証などさまざまなプラグインを配置すれば対応できるような形です。
たとえば,APIのプラグインであれば,Twitterならtwitter.py, mixiならmixi.pyというコードの中に,サービスに依存するコードがすべて集約されています。問題の切り分けがしやすく,メンテナンスコストが低い構成というのを常に意識しています。
実際はファイルを置いただけで機能を追加できるようにでもできますが,ファイルのリスティングなどのパフォーマンスをちょっとでも稼ぐために,プラグインモジュールの__init__.pyにリスト1のようなデータを定義しているので,そこは合わせて修正する必要はあります。
リスト1
SERVICE_SUPPORTS = [
{
'key':'twitter',
'api':API_TYPE_TWITTER,
'auth':AUTH_TYPE_BASIC,
'type':SERVICE_TYPE_NO_REQUIRE,
},
{
'key':'hatena_haiku',
'api':API_TYPE_HATENA_HAIKU,
'auth':AUTH_TYPE_BASIC,
'type':SERVICE_TYPE_NO_REQUIRE,
},
...
]
MetaGatewayのプラグイン実装
まずはMetaGatewayのコードを実際に出しながら,どういう構造になっているのかを話していこうと思います。まず具体的に,それらのプラグインがどのように実装されているのかを簡単に説明します。例として,マイクロブログに対応するためのプラグイン部分をとりあげます。マイクロブログの代表的なものに,Twitter, はてなハイク,Jaiku,cotobako,Haru.fm,もごもご,piyo,Timelog,Wassr等があります。
まず,すべてのAPIプラグインはApiPlugin抽象クラス(リスト2)からの派生クラスになります。これらのメソッドをオーバーライドすることにより,挙動を制御することができます。インターフェースに関しては,XMLRPCのインターフェース(blogger,metaweblog, movabletype, typepad)とatompubを抽象化したあとに,各種APIを参考にすべてを表現するために足りないものを追加したものになります。
リスト2
class ApiPlugin():
DEFAULT_CHANNEL_ID = '1'
def __init__(self, end_point, auth):
self.end_point = end_point
self.auth = auth
self.error_msg = None
def isAuthed(self):
return False
def getUserInfo(self, params):
return False
def getUserChannels(self, params):
return False
def getPost(self, params):
assert( params.has_key('post_id') )
post_id = params[ 'post_id']
documents = self.getRecentPosts(params)
for document in documents:
if( document['post_id'] == post_id ):
return document
raise ApplicationException()
def getRecentPosts(self, params):
return []
def createPost(self, params):
raise ApplicationException()
def isCreatePost(self):
return True
def deletePost(self, params):
raise ApplicationException()
def editPost(self, params):
raise ApplicationException()
def createUploadFile(self, params):
raise ApplicationException()
def getUploadFiles(self, params):
raise ApplicationException()
def deleteUploadFile(self, params):
raise ApplicationException()
def getCategories(self, params):
return []
def getPostCategories(self, params):
return []
def setPostCategories(self, params):
return True
def getTrackbackPings(self, params):
return []
def canTrackback(self, params={}):
return False
def rebuildSite(self, params):
return True
def formatTitle(self, title = ''):
return title
def formatBody(self, body = ''):
return body
boundary = None
def makeBoundary(self):
if( self.boundary == None ):
self.boundary = '----------------------------' + str(int(time.time()))
return self.boundary
MULTIPART_CONTENT_TYPE = 'multipart/form-data'
def makeMultipartHeader(self):
content_type = ("%s; boundary=%s" % (self.MULTIPART_CONTENT_TYPE, self.makeBoundary()))
return {'Content-Type':content_type}
def makeMultipartBody(self, params, exinfo={}):
boundary = self.makeBoundary()
orderd = sorted(params.keys())
s = ''
for k in orderd:
s += ( '--' + boundary + "\r\n" )
if( exinfo.has_key(k) ):
s += ('Content-Disposition: form-data; name=\"%s\"; filename="%s"\r\n' % (k, exinfo[k]['filename']) )
s += ('Content-Type: %s' % (exinfo[k]['content-type']) + "\r\n")
s += ('Content-Length: %s' % len(params[k]) + "\r\n\r\n")
else:
s += ('Content-Disposition: form-data; name=\"%s\"\r\n\r\n' % (k) )
s += (params[k] + "\r\n")
s += ( '--' + boundary + '--\r\n\r\n' )
return s
リスト2のコードは,だいたい先に挙げたインターフェースと1対1にマップされているのですが,いくつかわかりにくいものもあるので説明しておきます。
isAuthedは,getUserChannels時に認証できない場合,実際に記事を取得してみて認証を行うためにあります(ただ,実際には書き込んでみないと合っているかどうが一切かわからないサイトも多く,「開発者泣かせ」も結構あります)。
isCreatePostは,本当にデータが書き込まれたかをチェックするためにあります(Twitterなどでは,連続で同じステータスを書き込むと成功であるにもかかわらず,1つにまとめられてしまい書き込まれないため)。
rebuildSiteは,MovableTypeのようにmt.publishPostしないと再構築されないサイトが数多くあるので,そのためにあります。formatTitle, formatBodyはMicroformatsなどが必要なサイトの場合,そこを変換するためにあります。さらに,Google App Engineのfetchでは,postのContent-Typeが常にapplication/x-www-form-urlencodedのになってしまうので,multipart/form-dataできるようにメソッドを追加しています。

