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

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

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

プラグイン作成の実際

実際のTwitterプラグインはリスト3のようになります。これに関しても,Twitter互換のAPIをもつサイトが多いので,書き換えられるようにしているわけです。そのため,Twitterから派生しているmogomogoは,リスト4のように簡単に記述することができます。また「はてなハイク」なども同様にTwitter互換のAPIですので,非常に簡単に実装することができるわけです。

リスト3

class ApiPlugin_twitter(ApiPlugin):
    DATE_FORMAT = '%a %b %d %H:%M:%S +0000 %Y'
    MAX_POST_LEN = 140
    RETURN_ID_NODE = 'id'

    _capable = CHANNEL_CAPABLE_ALL & ~CHANNEL_CAPABLE_EDIT \
                                   & ~CHANNEL_CAPABLE_HTML \
                                   & ~CHANNEL_CAPABLE_UPLOAD \
                                   & ~CHANNEL_CAPABLE_SEARCH 

    def _getTitle(self, text, keyword):
        return text
    def _getText(self, text, keyword):
        return text
    def _getPubDate(self, text):
        return datetime.datetime.strptime(text, self.DATE_FORMAT)
    def _getFeedUrl(self, params = {}):
        return self.end_point + 'statuses/user_timeline/' + self.auth.username + '.xml'
    def _getPostUrl(self, params = {}):
        return self.end_point + 'statuses/update.xml'
    def _getDeleteUrl(self, params = {}):
        id = params['id']
        return self.end_point + ( 'statuses/destroy/%(id)s.xml' % { 'id': id } )

    def getUserChannels(self, params = {}):
        channels = [
            { 'channel_id' : self.DEFAULT_CHANNEL_ID, 
              'name' :self.auth.username,
              'url' : self.end_point + self.auth.username,
              'capable': self._capable, },
        ]
        return channels

    def getRecentPosts(self, params = {}):
        headers = self.auth.getHeaders()
        uri = self._getFeedUrl()
        result = urlfetch.fetch(uri, None, urlfetch.GET, headers, False)
        if( result.status_code >= 400 ):
            raise ApplicationException()

        root = cElementTree.fromstring(result.content)
        entries = []
        tag = './status'
        for status in root.findall(tag):
            pub_date = self._getPubDate(status.find('created_at').text)
            id = status.find('id').text
            text = status.find('text').text
            keyword = status.find('keyword')
            if( keyword != None ):
                keyword = keyword.text
            entry = {
                'post_id': id,
                'link': self.end_point + self.auth.username + '/status/' + id,
                'title': self._getTitle(text, keyword),
                'text': self._getText(text, keyword),
                'pub_date' : pub_date,
            }
            entries.append(entry)
        return entries

    def createPost(self, params = {}):
        headers = self.auth.getHeaders()
        uri = self._getPostUrl()
        text = params['text']

        check_text = text.replace('\n', '')
        l = len(check_text)
        if(len(check_text) <= 0):
            text = params['title']

        query = {
            'status': text,
            'source': settings.APPLICATION_OWNER,
        }
        payload = urllib.urlencode(query)
        result = urlfetch.fetch(uri, payload, urlfetch.POST, headers, False)
        if( result.status_code >= 400 ):
            raise ApplicationException()
        root = cElementTree.fromstring(result.content)
        id = root.find(self.RETURN_ID_NODE).text
        return {
            'complete':True, 
            'post_id':id,
            'link': self.end_point + self.auth.username + '/status/' + id,
            'title': text,
            'original_text': text,
            'pub_date': params['pub_date'],
        }

    def isCreatePost(self, params = {}):
        # TODO database compare
        return True

    def deletePost(self, params = {}):
        headers = self.auth.getHeaders()

        id = params['post_id']
        uri = self._getDeleteUrl( {'id':id} )
        result = urlfetch.fetch(uri, None, urlfetch.POST, headers, False)
        if( result.status_code >= 400 ):
            raise ApplicationException()
        root = cElementTree.fromstring(result.content)
        deleted_id = root.find(self.RETURN_ID_NODE).text
        if( id == deleted_id ):
            return True

        raise ApplicationException()

リスト4

class ApiPlugin_mogomogo(twitter.ApiPlugin_twitter):
    DATE_FORMAT = '%a %b %d %H:%M:%S +0900 %Y'
    HOME_URL = 'http://mogo2.jp/home'
    RETURN_ID_NODE = './status/id'

    _capable = CHANNEL_CAPABLE_ALL & ~CHANNEL_CAPABLE_EDIT \
                                   & ~CHANNEL_CAPABLE_DELETE \
                                   & ~CHANNEL_CAPABLE_HTML \
                                   & ~CHANNEL_CAPABLE_UPLOAD \
                                   & ~CHANNEL_CAPABLE_SEARCH 

    def _getFeedUrl(self, params = {}):
        return self.end_point + 'statuses/user_timeline.xml'
    def _getPostUrl(self, params = {}):
        return self.end_point + 'statuses/update.xml'
    def _getPubDate(self, text):
        dt = datetime.datetime.strptime(text, self.DATE_FORMAT)
        offset = 60*60*9
        of = datetime.timedelta(seconds=offset)
        return dt - of
    def getUserChannels(self, params = {}):
        channels = [
                { 'channel_id' : self.DEFAULT_CHANNEL_ID, 
                  'name' : self.auth.username,
                  'url' : self.HOME_URL,
                  'capable': self._capable,
            }
        ]
        return channels

    def isCreatePost(self, params = {}):
        return True

    def deletePost(self, params = {}):
        raise ApplicationException()

日付だけを見てもマイクロブログごとに好きなフォーマットを使っているので,残念ながら一貫性がほとんどありません。

表1 Twitter系マイクロブログの日付フォーマット

twitter'%a %b %d %H:%M:%S +0000 %Y'
mogomogo'%a %b %d %H:%M:%S +0900 %Y'
nowa'%Y-%m-%d %H:%M:%S'
timelog'%Y/%m/%d %H:%M:%S'
はてなハイク'%Y-%m-%dT%H:%M:%SZ'

これらも含めて各マイクロブログごとに微妙な差異があるため,各専用クライアントはそれらを吸収する必要があり,それぞれ対応しなければなりません。なぜ再発明したがるのかがいまひとつ理解できませんが,真似るところは真似る,特化する所は特化する。メリハリが大事なのではないかなと思います。

ちなみに,私が開発しているしゃべるでは完全Twitter互換なAPIを提供しています。そのためHOSTSにtwitter.comと社内サーバのマッピングを記述すれば,どのような専用クライアントを使っても動作します。HOSTSの書き換えを行いたくない場合は,P3のような優れたクライアントを使えば動作します。

ユーザの「入り口」を広げたい

最近のWebアプリケーションは,Webインターフェースだけに特化したものでなくなってきています。スマートフォン,モバイル,ブラウザのExtension,専用クライアントなど,どれもシームレスに連携できるのが当たり前になりつつあります。DropBoxなどのようにExplorerの拡張になっているものもあります。ユーザの入り口を限りなく広めていく必要があるのです。MetaGatewayでもそれらの入り口を広げるためにAPIを実装しています。そのため,どのブログクライアントでも同様に操作することが可能になるのです。

よく言われるのですが,なぜにAtomPubでなくて,MetaWeblogなのか? 理由は明白です。MetaWeblogの方が仕様が明確だからです。機能も豊富ですし,ほとんどのブログクライアントで実装されています。もちろんGoogleのAPIのようなAtomPubの拡張はいいと思いますし,AtomPubの方が技術的にはエッジが利いているかもしれません。ただ,私にとっては最も重要なことではありません。技術云々よりも,まずたくさんの人に喜んでもらうにはどうするのが一番いいのか? それが大切だと考えています。

著者プロフィール

Techmonkey

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

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