MetaGatewayに見る次世代Webサービス

第4回 プラグイン実装でわかるMetaGatewayのコンセプト

この記事を読むのに必要な時間:およそ 6 分

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できるようにメソッドを追加しています。

著者プロフィール

Techmonkey

コンビのWebビジネスプロデューサー。

社内マイクロブログ - しゃべるエンタープライズサーチ - どこかな?次世代投稿管理サービス - MetaGateway などを2人で開発。今はクラウド型の究極のチームコラボレーションソフトウェアを開発中です。

コメント

コメントの記入