2009年11月1日日曜日

Google Wave: 「てめーはおれを怒らせた」ボットを作ってみた

このブログ記事をはてなブックマークに追加

「てめーはおれを怒らせた」ボットといきなり言われても何のことだか分からないと思うけど、要はユーザの発言に含まれる特定の文字に反応して動作するGoogle Waveのボット billowlet@appspot.com を作成したのだ。

追記(2010/3/7): Google Wave Robots API v2がリリースされたので、新しいAPIでボットを書き直してみた。古いAPIは今後使用できなくなるようなので、今のうちに新しいAPIに移行した方が良いと思う。


先日、Google Waveの開発者用アカウントをGoogleからもらえたので早速何か作ってみることにした。どうせならGoogle App Engineを利用したかったので簡単に作れそうなボット(bot)を作成してみたわけだ。因みに元ネタのジョジョの奇妙な冒険とは言い回しが似ているというだけで内容とは関係ないので悪しからず。

このボットはユーザの発言内容に含まれる特定の文字に反応して動作し、ネコみたいな喋り方にさせられたり、英語や中国語で喋らされたり、笑いっぱなしにさせられたり、どもらされたりと、ユーザの発言がいろいろ変化する。因みに、「ごめんなさい」もしくは「ゆるして」と言うことで、動作を取り消すことができる。画像は実際の使用例だ。

ボットのアカウントはbillowlet@appspot.comで、使い方はGoogle WaveのContactsパネル内にある+ボタンでこのアカウントを追加するだけだ。そして、このボットアカウントをWaveに参加させればよい。因みに、ボットに使われている仔猫のアイコンについてはフリー素材ROKOの画像を利用させてもらった。

以下に「てめーはおれを怒らせた」ボットのデモのリンクを張っておくが、こちらはWave Sandboxアカウント所持者のみが利用できる。

「てめーはおれを怒らせた」ボット デモ

実際に使ってみて、このGoogle Waveはとても面白いWebアプリケーションだと思った。今回のアプリケーション作成でも将来性を強く感じることができたので、これから多くの人が使うことでどのように発展していくのか楽しみだ。

以下にGoogle App Engine上のソースコードを示しておく。ところで、stringモジュールのtranslate関数がunicodeに対してちゃんと動作しないのは何とかならないものかな。

追記(2009/11/23):

開発者用アカウント(Sandbox)では正常に動作するボットの発言が、Previewアカウントでは表示されないので調べてみたら、どうやらGAEにおけるGoogle Wave APIのバグのようだ。現在、GAEでCreateBlip/CreateChildを使うと正常に動作しない。今のところ対策はないようなので、修正されるまで待つしかないようだ。因みにそれ以外のAPIは動作するようで、ボットの発言はないが自分の発言はちゃんとボットによって変更されて表示される。

追記(2009/11/24):

Google Wave APIのバグが直って、Previewアカウントでも正常に動作するようになったみたい。しかし、今度はしばらくするとボットが反応しなくなるバグがあるようだ。プレビュー版だし不具合が出るのは仕方がないな。未知のバグを見つけたらできるだけ報告するようにしよう。

追記(2009/11/27):

ボットが反応しなくなるバグが直ったようだ。これでボットの開発については問題なくなったかな。

追記(2010/1/31):

最近、ボットのアイコンや名前が表示されないと思っていたら、ボットのプロフィールが表示されないバグがあるみたい。

billowlet.py

#!/usr/bin/env python # -*- coding: utf-8 -*- import re, string, urllib, urllib2, random from google.appengine.ext import db from waveapi import events from waveapi import model from waveapi import robot from waveapi import document from waveapi import simplejson HIRAGANA = u"あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよわをん" KATAKANA = u"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨワヲン" class UserData(db.Model): user = db.StringProperty(multiline=False) status = db.StringProperty(multiline=False) def OnParticipantsChanged(properties, context): added = properties["participantsAdded"] for participant in added: if participant != "billowlet@appspot.com": Notify(context, participant) def OnRobotAdded(properties, context): root_wavelet = context.GetRootWavelet() root_wavelet.CreateBlip().GetDocument().SetText(u"おれはbillowlet。よろしくな。") def Translate(text, from_lang, to_lang): url = "http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q=%s&langpair=%s%%7C%s" % (urllib.quote(text.encode("utf-8")), from_lang, to_lang) data = simplejson.loads(urllib2.urlopen(url).read()) return data["responseData"]["translatedText"] def BeCat(text): text = re.sub(u"。", u"にゃ。", text) text = re.sub(u"?|\?", u"にゃぁ?", text) text = re.sub(u"!|\!", u"にゃっ!", text) text = re.sub(u"な", u"にゃ", text) if text[-1] not in (u"。", u"?", u"?", u"!", u"!"): text += u"にゃ" return text def DotMarks(text): from_kana = u"かきくけこさしすせそたちつてとはひふへほカキクケコサシスセソタチツテトハヒフヘホ" to_kana = u"がぎぐげござじずぜぞだぢづでどばびぶべぼガギグゲゴザジズゼゾダヂヅデドバビブベボ" for f, t in zip(from_kana, to_kana): text = text.replace(f, t) return text def Laugh(text, ch): for i in range(len(text), 0, -1): text = text[:i] + ch + text[i:] return text.replace(u"。", u"").replace(u"、", u"") + ch * random.randint(3, 10) def Kana(idx): return HIRAGANA[idx] + u"|" + KATAKANA[idx] def OnBlipCreated(properties, context): blip = context.GetBlipById(properties["blipId"]) doc = blip.GetDocument() user = blip.GetCreator() data = UserData.all().filter("user =", user).fetch(1) if data: status = data[0].status else: status = None text = doc.GetText().strip() if status and re.match(u"ごめんなさい|ゆるして", text): blip.CreateChild().GetDocument().SetText(u"ゆるしてやる。やれやれだぜ。") if data: data[0].delete() angry = range(len(HIRAGANA)) random.shuffle(angry) if status: if status == u"ネコになれ。": text = BeCat(text) doc.SetText(text) elif status == u"英国紳士になれ。": text = Translate(text, "ja", "en") doc.SetText(text) elif status == u"台湾に住め。": text = Translate(text, "ja", "zh-TW") doc.SetText(text) elif status == u"インチキ日本人になれ。": text = Translate(Translate(text, "ja", "en"), "en", "ja") doc.SetText(text) elif status == u"大事なことは2回言え。": doc.SetText(text + text + u"\n大事なことなので2回言いました。") elif status == u"笑え。": doc.SetText(Laugh(text, u"w")) elif status == u"濁れ。": doc.SetText(DotMarks(text)) elif status == u"逆さまになれ。": doc.SetText(text[::-1]) elif status == u"どもれ。": doc.SetText((text[0] + u"、") * 3 + text) elif status == u"うざくなれ。": doc.SetText(Laugh(text, u"っ")) else: userdata = UserData() if re.search(Kana(angry[0]), text): status = u"ネコになれ。" elif re.search(Kana(angry[1]), text): status = u"英国紳士になれ。" elif re.search(Kana(angry[2]), text): status = u"台湾に住め。" elif re.search(Kana(angry[3]), text): status = u"インチキ日本人になれ。" elif re.search(Kana(angry[4]), text): status = u"大事なことは2回言え。" elif re.search(Kana(angry[5]), text): status = u"笑え。" elif re.search(Kana(angry[6]), text): status = u"濁れ。" elif re.search(Kana(angry[7]), text): status = u"逆さまになれ。" elif re.search(Kana(angry[8]), text): status = u"どもれ。" elif re.search(Kana(angry[9]), text): status = u"うざくなれ。" if status: blip.CreateChild().GetDocument().SetText(u"てめーはおれを怒らせた。%s" % status) userdata.user = user userdata.status = status userdata.put() def Notify(context, participant): root_wavelet = context.GetRootWavelet() root_wavelet.CreateBlip().GetDocument().SetText(u"%s、おれを怒らせるなよ。" % participant.split("@")[0]) def main(): myRobot = robot.Robot("billowlet", image_url="http://billowlet.appspot.com/assets/icon.gif", version="1", profile_url="http://billowlet.appspot.com/") myRobot.RegisterHandler(events.WAVELET_PARTICIPANTS_CHANGED, OnParticipantsChanged) myRobot.RegisterHandler(events.BLIP_SUBMITTED, OnBlipCreated) myRobot.RegisterHandler(events.WAVELET_SELF_ADDED, OnRobotAdded) myRobot.Run() if __name__ == "__main__": main()

app.yaml

application: billowlet version: 1 runtime: python api_version: 1 handlers: - url: /_wave/.* script: billowlet.py - url: /assets static_dir: assets

0 コメント: