2009/09/07
Google App Engine Oil で twitter もどき STEP 1
前回、Webアプリケーションをさくっと手軽に構築できてしまう Google App Engine (GAE) を試してみましたが、今回は、もう少しまともなアプリケーションを作ってみます。
作成する前に、つい先日 GAEO が 0.3 にバージョンアップしているようなので、早速最新のリリースを試してみましょう。
バージョンアップ作業は、環境変数のパスを新しいバージョンのものに変更しただけです。
Twitter は、140文字以下の短いメッセージをつぶやきあうシンプルなサービスです。現在とても注目されているサービスですね。
シンプルでわかりやすいですので、今回は Twitter もどきのアプリケーションを作成してみます。
mockker
それでは、Twitter もどきの「mockker」というプロジェクトを作成します。
とりえあず今回は、データの登録ができればOKとします。また、アカウントは Google App Engine の ユーザーサービスを使用し、Google アカウントと連携させます。
gaeo.py mockker
cd mockker
まずは、つぶやきの scaffold を作成します。
gaeogen.py scaffold status create show "text:StringProperty()" "user:UserProperty()" "created_at:DateTimeProperty(auto_now_add=True)"
ルーティング
デフォルトのルートへのルーティングを、welcome/index から status/index へ変更します。
gaeo/dispatch/router.pyclass Router:
""" Handles the url routing... """
class __impl:
def __init__(self):
self.__routing_root = {'controller': 'status',
'action': 'index'}
model
scaffold により、 application/model/status.py が作成されます。
application/model/status.pfrom google.appengine.ext import db
from gaeo.model import BaseModel, SearchableBaseModel
class Status(BaseModel):
created_at = db.DateTimeProperty(auto_now_add=True)
text = db.StringProperty()
user = db.UserProperty()
controller
scaffold により作成された status コントローラを次のように修正します。
application/controller/status.pyimport cgi
import logging
from google.appengine.ext import db
from google.appengine.api import users
from gaeo.controller import BaseController
from model.status import Status
class StatusController(BaseController):
def create(self):
r = Status(
# Uncomment all required properties here.
# text = self.params.get('text', None),
# created_at = self.params.get('created_at', None),
user = users.get_current_user(),
)
for prop in Status.properties():
if prop in self.params:
setattr(r, prop, self.params.get(prop))
r.put()
self.flash['notice'] = u"独り言を更新したよ"
self.redirect('/')
def index(self):
notice = self.flash.get('notice', '')
self.msg = notice
if self.current_user:
query = Status.all().filter('user = ', self.current_user)
query.order("-created_at")
self.result = query.fetch(limit=10)
def show(self):
r = Status.get(self.params.get('id'))
if r:
for prop in Status.properties():
setattr(self, prop, getattr(r, prop))
else:
self.redirect('/')
ユーザー認証のため、base コントローラの before_action を次のように修正します。
これにより、全てのアクションの前にユーザー認証が実行されます。
from google.appengine.api import users
def before_action(self):
self.current_user = users.get_current_user()
if not self.current_user:
self.signed_in = False
self.login_url = users.create_login_url(self.request.uri)
#self.redirect(users.create_login_url(self.request.uri))
else:
self.signed_in = True
self.logout_url = users.create_logout_url(self.request.uri)
self.nickname = self.current_user.nickname()
view
view を次のように修正します。
application/templates/base.html<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/css/common.css" type="text/css" media="screen" />
{% if signed_in %}
<link rel="stylesheet" href="/css/login.css" type="text/css" media="screen" />
{% endif %}
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script type="text/javascript" src="/js/application.js"></script>
</head>
<body>
<div id="header">
<div id="header_inner">
<h3><a href="/" title="Mockker">Mockker</a></h3>
{% if signed_in %}
<ul id="nav">
<li><a href="{{ logout_url }}" title="Logout">Logout</a></li>
</ul>
{% else %}
<ul id="nav">
<li><a href="{{ login_url }}" title="Login">Login</a></li>
</ul>
{% endif %}
</div>
</div>
<div id="content">
<div id="content_inner">
{% if msg %}
<div class="flash">
<div class="notice">
<p>{{ msg }}</p>
</div>
</div>
{% endif %}
{% if signed_in %}
<div id="update">
<form method="post" action="/status/create">
<span id="countdown">140</span>
<label for="text">何か言いたいことは?</label>
<textarea id="text" name="text" rows="10" cols="10"></textarea>
<input type="submit" name="update" value="Update" />
</form>
</div>
{% endif %}
{% block content %}{% endblock %}
</div>
</div>
<div id="footer">
<div id="footer_inner">
<div class="column">
<ul>
<li><a href="/" title="Home">Home</a></li>
</ul>
</div>
</div>
</div>
</body>
</html>
application/templates/status/index.html
{% extends "../base.html" %}
{% block title %}StatusController#index{% endblock %}
{% block content %}
<div id="statuses">
{% for r in result %}
<div id="status_{{ r.key }}" class="status">
<div class="info">
<p class="who_when">
{{ r.user.nickname }}<br />
{{ r.created_at }}
</p>
<p class="actions">
</p>
</div>
<div class="text">
{{ r.text }}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
あとは、Javascript や CSS などで、見栄えを整えます。
実行
dev_appserver.py .
登録したデータをクリアしたい場合は、「--clear_datastore」オプションをつけて実行します。
dev_appserver.py --clear_datastore .
以上で、自分だけがつぶやくことができる、独り言アプリケーションの完成ですw
次回は、他人のつぶやきを表示できるようにしてみたいと思います。
2009/08/04
Google App Engine Oil
Webアプリケーションをさくっと手軽に構築できてしまう Google App Engine (GAE) ですが、この Google 製のエンジンに、あるオイルを入れてやると、さらに激速なマシンができるようです。
Google App Engine Oil(GAEO) というこのオイルは、Ruby on Rails を参考にして作られた App Engine 専用の レーシングスペックのエンジンオイル(Webアプリケーションフレームワーク)です。
GAEO をインストール
まず、GAEO は GAE 上で実行されますので、予め Python や、GAE SDK をインストールします。
なお、以下は Windows での解説です。
プロジェクトホーム からパッケージ(gaeo-0.2.1.zip など)をダウンロードし、好きなところに解凍します。
GAEO スクリプトを利用するために、解凍先の bin ディレクトリへのパスを、環境変数 PATH に追加します。
パスを設定しましたら、gaeo.py コマンドを実行し、下記のようになるとインストール完了です。
>gaeo.py
Usage: C:\<install dir>\bin\gaeo.py <project name>
Hello GAEO
では、早速 Hello GAEOプロジェクトを作ってみます
GAEO でのプロジェクトを作成するには、作業ディレクトリで、次のコマンドを実行します。
gaeo.py hello
hello プロジェクトを作成すると、hello ディレクトリには、下記のコンテンツが生成されます。Rails そっくりです。
- app.yaml, favicon.ico, main.py
app.yaml と main.py は、GAE のメイン設定ファイルです。
- application/
アプリケーションのコードの置き場所。基本的にこの中のファイルを編集していきます。
- assets/
javascript や css ファイルの置き場所。app.yaml で設定可能です。
- gaeo/
GAEO のコアライブラリ。GAEO のアップグレードは、このディレクトリのファイルを入れ替えるとOKです。
- plugins/
プラグインのインストールディレクトリです。
アプリケーションの起動と確認
次のコマンドで GAE を起動し、作成したアプリケーションの動作を確認します。
なお、コマンドはアプリケーションのルートディレクトリで実行します。
hello>dev_appserver.py .
Running application hello on port 8080: http://localhost:8080
http://localhost:8080/ にアクセスして、「It works!!」と表示されればOKです。
なかなか刺激的な体験ですが、次はちょっとしたアプリケーションを作ってみたいと思います。
- Google App Engine Oil で twitter もどき
- Google App Engine Oil で twitter もどき STEP 1
参考:
2009/06/15
Google Developer Day 2009
今年も Google Developer Day に参加してきました。
すでに恒例のイベントとなっていますが、年々内容が洗練され、とてもすばらしい内容になってきました。
今回の主な内容は、次のもの。
なかでも、HTML5 と Google Wave はあらたな時代を感じさせるすばらしいものでした。
ブラウザだけで(HTML + Javascript)、2D や 3D といったリッチな表現を可能にする、HTML5。
これまでの メール 主体のコミュニケーションを大きく変えてしまうパワーがある、Google Wave。
実際に普及となるとまだ先だとは思いますが、
Android についても、もうすぐ日本でも docomo から発売されますので、これから非常に注目されるものです。
Open Social は、ついに日本最大の SNS である mixi が完全対応ということで、多くのユーザにリーチできるという点で、マーケティング的にも非常に魅力的なものになりました。
Android と Open Social の組み合わせは、非常に刺激的なものです。
そして、Google Apps と Google App Engine の組み合わせによる、Enterprise なアプローチは、クラウド化する今日において、対企業へ非常に魅力的なソリューションを提供できるようになります。
Google Maps は、久しぶりのバージョンアップにより、Ver 3 になります。
iPhone や、Android といったモバイル端末上で快適に動作するように最適化がなされるようです。
GPS が普及し、地図分野はますます便利になっていきますね。
今回の GDD では、Google I/O 同様サプライズとして、Google Dev Phone の新版が無料配布されました。
すでにあった Dev Phone 1 (Android 1.0) を、Android 1.5 にアップデートしようかと思っていたところだったので、ちょうどいいタイミングです。
しかも、ロケールに日本語があって、日本語での文字入力も可能。すばらしいです。
これで、Android 1.5 でも実機テストが可能ですね。
会場でも、iPhone を片手に講演を聴いている方が多数いて、やはり、昨年とは会場の雰囲気も違いました。
来年はきっと、僕も Android を片手に講演を聴いているでしょう・・。
思いついたアイディアが、たとえ先見の明があったとしても、それが時期尚早だったりすることがあります。
しかし、それが今ならなんだかいけそうな気がする!と、今年の GDDD は、そんな気になりました。
今一度、過去のアイディアを整理してみるのもいいかもしれません。
2009/04/17
Google Earth で、ツアーを作ってみる

先日 TechCrunch より、 Google Earthのフライスルーがブラウザで閲覧できるようになった という衝撃の記事を発見しました。
今週初め、GoogleはGoogle Earthツアーをブラウザから直接閲覧できるプラグインをリリースした。2月に公開されたGoogle Earthのリリース5.0でも導入されたツアー機能を使えばGoogle Earthで表示できる場所のどこででもバーチャル・フライスルーを作成することができる。これによってなかなか印象的な作品を作り出すこともできる。Googleではいくつものベストツアーをギャラリーに集めており、ハドソン川に緊急着水したFlight 1549の歴史的なフライトの再現やサンフランシスコの疾風ツアーが紹介されている。
というわけで、早速試してみました。
ビデオによるツアーの作成方法は こちら から。
そして、実際に作成してみたツアーが下のものです。プラグインをインストールすると見えるようになります。
次回の Google Developper Day 2009 の開催地である、パシフィコ横浜 へ一気にジャンプするだけですが、ブラウザだけで見れるのはなかなかですね。
サイトへの埋め込みは、 Embedded Tour Player Google gadget で、Google Earth で作成した KML ファイルを指定するだけ。あとは作成されたガジェットのコードをペーストすればいいんですね。
これはおもしろいです!
ラベル: gadget, google, googleearth
2009/01/23
[GAE]Google App Engine を使ってみる Hello, webapp World!
前回は、単純な CGI を試してみましたが、今回は、Google App Engine に提供されている webapp というフレームワークを使ってみます。
Hello, webapp!
webapp は次の3つのパートからなります。
- リクエストの処理と、レスポンスを生成する、RequestHandler クラス
- URL に従ってリクエストをルーティングする、WSGIApplication インスタンス
- WSGIApplication を起動する、メインルーチン
さっそく、helloworld.py を書き換えてみます。
helloworld/helloworld.py
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
class MainPage(webapp.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain'
self.response.out.write('Hello, webapp World!')
application = webapp.WSGIApplication(
[('/', MainPage)],
debug=True)
def main():
run_wsgi_app(application)
if __name__ == "__main__":
main()
開発サーバを起動し、http://localhost:8080 にアクセスします。
ブラウザに Hello, webapp World! が表示されれば成功です!
参考:
2009/01/22
[GAE]Google App Engine を使ってみる Hello, World!

昨年の4月にリリースされた、Google App Engine ですが、アカウントを申請したままずっと放置していました。
「一年の計は元旦にあり」ということで、今後ますますクラウド化していく世の中に備え、Google App Engine、Amazon Web Services は押さえておかなければなりません。
そこで、まずは Google App Engine を使ってみました。
使い始めるのは簡単で、
これですぐにローカルで開発が始められます。
Hello, World!
なにはともあれ、まずは Hello, World! ですね。
シンプルな Request Handler を作成
helloworld ディレクトリを作成し、その中に helloworld.py を作成します。
helloworld/helloworld.py
print 'Content-Type: text/plain'
print ''
print 'Hello, World!'設定ファイルを作成
helloworld ディレクトリに、app.yml を作成します。
helloworld/app.yml
application: helloworld
version: 1
runtime: python
api_version: 1
handlers:
- url: /.*
script: helloworld.pyアプリケーションのテスト
dev_appserver.py を実行し、ウェブサーバを起動します。実行するアプリケーションディレクトリのパスを渡します。
google_appengine/dev_appserver.py helloworld/開発サーバが起動したら、http://localhost:8080 にアクセスします。
ブラウザに Hello, World! が表示されれば成功です!
参考:
2008/12/24
Google Android Dev Phone 1

弊社CEOから、クリスマスプレゼントということで、開発者向けテスト機として Google から販売されている、Android Dev Phone 1 を渡されました。

使い始めるにあたり、次の事項に注意しました。
- アクティベーションに、データ接続が必要なので、あらかじめ Mopera U (docomo) を契約
- アクティベーション完了後に、Gmail 同期が行われるので、受信トレイをアーカイブしておく
SIMカードは、現在使用中のFOMA携帯用のものでOKでした。
気になるパケット代は、受信トレイをアーカイブしておいたので、mopera U の基本料金300円 + 200円程度のパケット料で済んだようです。
さっそく使ってみると、肝心の WiFi が認識しません!
あれこれ試して、結局、無線LANのチャネルの設定を変更して認識。接続成功です。
使用感は、iPhone に負けないくらいすばらしいですね。
まさに、PC と Mac といった感じでしょうか(良くも悪くも)。
iPhone のマルチタッチは使いやすいですが、Android のシングルタッチ + トラックボールもかなり使いやすいですね。
というわけで、日本で Android 携帯が発売される前には、なにかアプリを作ってみたいですね。
2008/06/12
Google Developer Day 2008
今年もGoogle Developer Dayに参加してきました。
基調講演では、Client / Connectivity / Cloud という「3つのC」が次世代Webのキーワードで、Googleではそれぞれ、Gears / Android / Google App Engine を提供しているというお話でした。
どれもまだ触っていないので、近いうちに弄ってみたいと思います。
午後のセッションでは、今回はコードラボ(その場で実際にコーティングしながらアプリを作る)という新しいタイプのセッションがあったので、最近流行りのOpenSocialのコードラボに参加しました。
どうせなら前回とはちょっと違ったものにと軽い気持ちだったのですが、OpenSocialをどのように利用するのかいまいちピンと来ていなかった自分には、他のメンバーとディスカッションすることができて、非常に参考になりました。
作成するアプリのアイディアは次のようなもの
- 自分と友達で動物占い
- 友達にタグ付
- チャット
- フィードリーダー
- 地図から友達検索
- 足跡
いくつかのグループに分かれてアプリを作成するのですが、上記の中でも一番ソーシャルっぽい占いアプリに参加
短い時間で、適当に作業担当を決めて作成していくのですが、みんなでワイワイやるといった感覚は普段はまったくないので、非常に新鮮な体験でした。作り終えたときの感動は格別です。
予習の際、デバッグにひどく苦労したのですが、コードラボではGoogleの方に教えて頂いた、CodeRunnerというアプリのおかげで、すぐさまTry and Errorができ非常に助かりました。
それと、いろいろなAPIが提供されているOpenSocialですが、やはり(APIだけでは)難しいところは、サーバ側で処理するのが常套手段とのことでした。

前回とはまた違った刺激を受けることができました。是非また参加したいと思いました。
2008/05/09
Gmail 2.0 で複数の署名を切り替える


僕は普段Gmailを使っていて、Greasemonkeyスクリプトの「Gmail Template Switch」を愛用しています。
複数のアカウントで使用したり、差出人によって署名・挨拶文を変えたい場合に非常に便利なGreasemonkeyスクリプトです。
Gmail Template Switch
大抵皆そうなんだろうけど、メールを書く場合決まった形があって、あいさつ文・本文・締め言葉・署名という順番で書いている。以前作ったスクリプト で、署名は差出人に応じて自動的に切り替わるようになったけど、あいさつ文や締め言葉は辞書に登録したりして、毎回入力してたわけです。会社で使っていることもあり、社内と社外で定型文が変わってくるので、辞書に登録した語句を忘れたりしてかなり不便だった。これはさすがに面倒なので、さらに Gmail を快適にすべく、テンプレートを切り替えられる Greasemonkey スクリプトを作ってみた。これで署名が複数あっても、定型文が複数あっても平気ですな。
Gmail にテンプレート切り替え機能を付けてみた - 記憶は削除の方向で
しかしながら、Gmail Template Switch は、Gmail 2.0では動作しないため、動作が快速でラベルの色分け等便利な機能は我慢し、日本語版Gmailを使っていたわけですが、先日ついに、日本語版Gmailも 2.0になってしまいました。
というわけで、Gmail Template Switch を、Gmail 2.0 で動作するようにハックしてみました。
Gmail Template Switcher - v 2.0
既存のデータは生かしたかったので、コードはほとんど流用しています。なお、jQueryの使用で意味不明なエラーが出たので、使わないようにハックしてます。
また、Gmail 2.0は、Greasemonkeyを正式にサポートするらしく、GmailGreasemonkey10APIが公開されているので、せっかくなので使ってみました。
はじめは軽い気持ちでやってみたのですが、Greasemonkeyは初めて、かつ、Gmail 2.0 になってずいぶん変わっているようで、とても苦戦しました・・・(汗
とくに、ElementのIDがランダムで変わるのと、Reply時の入力フォームが動的に挿入される点でかなりはまりました・・。
なお、操作方法等はオリジナルとかわりません。もちろん新しい機能なんてありませんw
以下、コード全文です。
※最新のコードはこちら
Update:
Safari (Greasekit), Google Chrome, Opera で動作を確認しました。
Update2:
Google Chrome Extension 版を作成しました。
gmailtemplateswitcherv20.user.js
// ==UserScript==
// @name Gmail Template Switcher - v 2.0
// @namespace http://www.r-stone.net/blogs/ishikawa
// @description Append the function to apply the mail template, when writing a mail. Modify source code of [Gmail Template Switch] from http://d.hatena.ne.jp/re_guzy by re_guzy
// @version 0.1.20080510.0
// @include http://mail.google.com/*
// @include https://mail.google.com/*
// @exclude http://mail.google.com/mail/help/*
// @exclude https://mail.google.com/mail/help/*
//
// Copyright (c) 2007-2008, re_guzy <goodspeed.xii@gmail.com>
// Distributed under the MIT license
// http://opensource.org/licenses/mit-license.php
// http://sourceforge.jp/projects/opensource/wiki/licenses%2FMIT_license
//
// Notice : To Uninstall this script, remove "gtssettings0-9" from Gmail contact list.
// Feature: When writing a mail, append a combobox to action. By selecting action,
// apply template to mail, or add template or remove template. Template
// is saved to "contact list" named starting with "gtssettings".
// Require: Greasemonkey 0.7.20080121.0
// ==/UserScript==
const DEBUG = false;
const KEY_TOKEN = "gts_token";
const KEY_CACHE = "gts_cache";
const CONTACT_NAME = "gtssettings";
const CONTACT_ID_RE = /\["\w+","(\w+)","gtssettings\d","gtssettings\d",/;
const MSGBODY_RE = /([\s\S]*)\n?(?:^---)(\n[\s\S]+)/m;
const LOCATION_RE = /(https?:\/\/[^\/]+\/(a\/[^\/]+\/)?).*/;
const SELECTOR = {
'gts' : 'descendant::*[local-name() = "select" or local-name() = "SELECT"][@id = "id_gts_template"]',
'input_form' : 'descendant::*[local-name() = "form" or local-name() = "FORM"]',
'body' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "body"]',
'subject' : 'descendant::*[local-name() = "input" or local-name() = "INPUT"][@name = "subject"]',
'to' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "to"]',
'from' : 'descendant::*[local-name() = "select" or local-name() = "SELECT"][@name = "from"] | descendant::*[local-name() = "input" or local-name() = "INPUT"][@name = "from"]',
'cc' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "cc"]',
'bcc' : 'descendant::*[local-name() = "textarea" or local-name() = "TEXTAREA"][@name = "bcc"]',
'gts_undo' : 'descendant::*[contains(concat(" ",@class," "), " gts_undo_option ")]',
'gts_first' : 'descendant::*[contains(concat(" ",@class," "), " gts_option_first ")]',
'discard' : 'descendant::*[local-name() = "button" or local-name() = "BUTTON"][count(preceding-sibling::*[local-name() = "button" or local-name() = "BUTTON"]) = 2]',
'labels' : 'descendant::*[local-name() = "span" or local-name() = "SPAN"]',
'msg' : 'div/div/div/div/div/div/div/div/div/div/div[3]/div/div[2]/div[2]/div/div[2]/div[2]/div/table/tbody/tr[2]/td[2]'
}
var T = new Array(10);
T[0] = { 'id' : -1, 'num' : 0 };
for (var i=1;i < T.length;i++) {
T[i] = {
'id' : -1,
'num' : i,
'from' : '',
'to' : '',
'cc' : '',
'bcc' : '',
'subject' : '',
'body' : '',
'body_latter' : ''
}
}
var Ja = false;
var recentView;
//Initialize gmail and gmonkey objects
window.addEventListener('load', function() {
if (unsafeWindow.gmonkey) {
unsafeWindow.gmonkey.load('1.0', function(gmail) {
function getViewType() {
var str = '';
switch (gmail.getActiveViewType()) {
case 'tl': str = 'Threadlist'; break;
case 'cv': str = 'Conversation'; break;
case 'co': str = 'Compose'; break;
case 'ct': str = 'Contacts'; break;
case 's': str = 'Settings'; break;
default: str = 'Unknown';
}
return str;
}
function getView() {
return gmail.getActiveViewElement();
}
function getMailForm() {
var a = xpath(SELECTOR['input_form'], getView());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getGts() {
var a = xpath(SELECTOR['gts'], getView());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getGtsOptUndos() {
return a = xpath(SELECTOR['gts_undo'], getGts());
}
function getGtsOptFirst() {
var a = xpath(SELECTOR['gts_first'], getGts());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getFrom(form) {
var a = xpath(SELECTOR['from'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getTo(form) {
var a = xpath(SELECTOR['to'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getCc(form) {
var a = xpath(SELECTOR['cc'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getBcc(form) {
var a = xpath(SELECTOR['bcc'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getSubject(form) {
var a = xpath(SELECTOR['subject'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getBody(form) {
var a = xpath(SELECTOR['body'], form || getMailForm());
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getDiscard() {
var a = xpath(SELECTOR['discard'], getMailForm().parentNode.parentNode);
if (a && a.length > 0) {
return a[0];
} else {
return null;
}
}
function getLabels(form) {
return a = xpath(SELECTOR['labels'], form || getMailForm());
}
function getCcLabel() {
var a = getLabels();
for (var i=0; i<a.length; i++) {
if (/Cc/.exec(a[i].innerHTML)) {
return a[i];
}
if (i>1) {
break;
}
}
return null;
}
function getBccLabel() {
var a = getLabels();
for (var i=0; i<a.length; i++) {
if (/Bcc/.exec(a[i].innerHTML)) {
return a[i];
}
if (i>1) {
break;
}
}
return null;
}
function getChangeLabel() {
var a = getLabels();
for (var i=0; i<a.length; i++) {
if (/change/.exec(a[i].innerHTML) || /変更/.exec(a[i].innerHTML)) {
return a[i];
}
if (i>1) {
break;
}
}
return null;
}
function switcher() {
str = getViewType();
if (str != "Compose" && str != "Conversation") {
if (recentView) {
recentView.removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
}
return;
}
recentView = getView();
window.setTimeout(function() {
initialize();
}, 600);
}
function nodeInsertedHandler(event) {
target = event.target;
if (target.nodeType == 1) {
tagName = target.tagName.toLowerCase();
if (tagName == 'form') {
log('form inserted');
window.setTimeout(function() {
initialize();
}, 500);
//getView().removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
} else if (tagName == 'table') {
log('table inserted');
window.setTimeout(function() {
initialize();
}, 500);
//getView().removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
}
}
}
function initialize() {
log('initialize');
try {
if (getGts()) {
log('already initialized');
return;
}
var form = getMailForm();
if (!form) {
log('form not found');
getView().addEventListener('DOMNodeInserted', nodeInsertedHandler, false);
return;
}
//getView().removeEventListener('DOMNodeInserted', nodeInsertedHandler, false);
var discard_button = getDiscard();
var label = discard_button.innerHTML;
Ja = (label == '破棄');
discard_button.parentNode.insertBefore(createSelectElement(), discard_button.nextSibling);
composeCommand(form);
} catch(e) {
log('add combobox failure because : ' + e);
return;
}
}
function createSelectElement() {
var content = document.createElement('select');
content.setAttribute("id", "id_gts_template");
content.setAttribute("style", "margin-left:10px;font-size:.8em;");
content.innerHTML = toOption('please wait...' , false , true);
content.addEventListener('change', function(event) {doCommand(event.target)}, true);
return content
}
function toOption(text, value, selected, cls, isDom) {
if (isDom) {
var attr = {
'style' : value ? null : 'color: rgb(119, 119, 119);',
'disabled' : value ? null : 'disabled',
'selected' : selected ? 'selected' : null,
'value' : value ? value : null,
'class' : cls ? cls : null
};
var elm = document.createElement('option');
for (var i in attr) {
if (attr[i]) {
elm.setAttribute(i, attr[i]);
}
}
elm.innerHTML = text;
return elm;
} else {
var attr = {
'style' : value ? null : '"color: rgb(119, 119, 119);"',
'disabled' : value ? null : '"disabled"',
'selected' : selected ? '"selected"' : null,
'value' : value ? '"' + value + '"' : null,
'class' : cls ? cls : null
};
var a = [];
for (var i in attr) {
if (attr[i]) {
a.push(i + "=" + attr[i]);
}
}
return "<option " + a.join(' ') + ">" + text + "</option>";
}
}
function composeCommand(form) {
getTemplates(function /*parseTemplate*/(notes, use_cache) {
for (var i in notes) {
if (use_cache) {
T[i] = notes[i];
} else {
var note = notes[i].note ? decode(notes[i].note, false) : "{}";
try {
T[notes[i].num] = eval(note);
T[notes[i].num].id = notes[i].id;
T[notes[i].num] = decode(T[notes[i].num], true);
} catch(e) {log("eval failed : " + e);}
}
}
recomposeSelectElement(form);
applyDefault(form);
if (notes.length == 0) {
save();
} else if (!use_cache) {
var caches = [];
for (var i in T) {
var encoded = encode(T[i], true);
if (encoded) {
caches.push(encoded.toSource());
}
}
GM_setValue(KEY_CACHE, "[" + caches.join(", ") + "]");
}
});
}
function applyDefault(form) {
var from = getFrom();
if (from) {
var fromvalue = from.value;
matched = grep(T, function(i) {
return (i.name + '').indexOf('#') == 0 && i.from == fromvalue;
});
if (matched.length > 0) {
applyTemplate(matched[0].num, form);
}
}
}
function recomposeSelectElement(form) {
var options = [];
options.push(toOption(trans("Template actions...") , "init" , true, "gts_option_first"));
var enables = grep(T, function(o) {return (o.name && o.num != 0);});
var expand = function(arrays, cmd) {
var hash = {};
for (var i in enables) {
hash[enables[i].from] = hash[enables[i].from] || [];
hash[enables[i].from].push(enables[i]);
}
for (var i in hash) {
arrays.push(toOption(' <' + (i || trans('No from')) + '>'));
for (var j in hash[i]) {
arrays.push(toOption(' ' + hash[i][j].name , cmd + '_' + hash[i][j].num));
}
}
};
var tmp = [
{'cmd':'apply', 'exp':trans("Apply"), 'func':expand},
{'cmd':'add','exp':trans("Append"), 'func':function(arrays, cmd) {
var used = grep(T , function(o) { return (o.name && o.num != 0) });
if (used.length < 9) {
arrays.push(toOption(' ' + trans("Includes from") , cmd));
arrays.push(toOption(' ' + trans("Excludes from") , cmd + '_ignore_from'));
} else {
arrays.push(toOption(' ' + trans('Quantity limit is 9')));
}
}},
{'cmd':'delete','exp':trans('Remove'), 'func':expand}
];
for (var i in tmp) {
if (tmp[i].func != expand || enables.length != 0) {
options.push(toOption('-------'));
options.push(toOption(trans('verbs', tmp[i].exp) + ':'));
tmp[i].func(options, tmp[i].cmd);
}
}
var gts = getGts();
gts.innerHTML = options.join('');
gts.value = 'init';
}
function doCommand(selectNode) {
var form = getMailForm();
if (form) {
if (selectNode.value == 'add') {
addTemplate(form, true);
} else if (selectNode.value == 'add_ignore_from') {
addTemplate(form, false);
} else if (selectNode.value.match(/apply_(\d+)/)) {
applyTemplate(RegExp.$1 , form);
} else if (selectNode.value.match(/delete_(\d+)/)) {
deleteTemplate(RegExp.$1 , form);
} else if (selectNode.value == 'undo') {
if (unsafeWindow.gts_undo) { unsafeWindow.gts_undo(); }
}
selectNode.value= 'init';
} else {
log('form not found');
}
}
function addTemplate(form, contain_from) {
var m = trans('Please input the template name.');
if (contain_from) {
m += '\n';
m += trans('If the name is started from "#", it becomes default of the corresponding "from".');
}
var name = window.prompt(m, "");
if (!name) {return;}
if (grep(T , function(o) { return (o.name == name) }).length > 0) {
alert(trans('The name already exists.'));
return;
}
var empties = grep(T , function(o) { return (o.name || o.num == 0) }, true);
var t = empties[0];
var to = getTo(form);
var b = getBody(form);
var c = getCc(form);
var bc = getBcc(form);
var f = getFrom(form);
var s = getSubject(form);
T[t.num] = {
'num' : t.num,
'id' : t.id,
'name' : name,
'from' : contain_from ? f.options[f.selectedIndex].value : "",
'to' : to.innerHTML,
'cc' : c.innerHTML,
'bcc' : bc.innerHTML,
'subject' : s.value,
'body' : b.innerHTML
};
if (MSGBODY_RE.exec(T[t.num].body)) {
T[t.num].body = RegExp.$1;
T[t.num].body_latter = RegExp.$2;
}
editContact(T[t.num], function() {
recomposeSelectElement(form);
msg(trans('appended', name));
});
}
function applyTemplate(num , form) {
if (typeof T[0]._init == 'undefined') {
T[0]._init = {};
var selectors = x('f,s,t,c,bc,bo');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
T[0]._init[j.name] = trim(j.value);
}
}
var selectors = x('f,s,t,c,bc');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
var tmp = T[0]._init[j.name];
if (j.name != 'from' || j.value != T[num][j.name]) {
if (j.type == 'textarea') {
var targetaddrs = tmp || "";
//既に含まれているものは追加しない
var notcontains = grep(T[num][j.name].split(','), function(i) {
if (trim(i).length == 0) { return; }//空白は無視
if (/([\w\.+-]+@[\w+-]+(\.[\w+-]+)+)/.exec(i)) {
return targetaddrs.indexOf(RegExp.$1) < 0;
} else {
return targetaddrs.indexOf(i) < 0;
}
});
if (tmp) { notcontains.unshift(tmp); }
j.value = notcontains.join(', ');
} else {
j.value = (j.name == 'subject' && tmp) ? tmp : T[num][j.name];
}
if (j.name == 'cc' && T[num][j.name]) {
var cc_label = getCcLabel();
if (cc_label) {
emulate_click(cc_label);
}
}
if (j.name == 'bcc' && T[num][j.name]) {
var bcc_label = getBccLabel();
if (bcc_label) {
emulate_click(bcc_label);
}
}
}
}
var change_label = getChangeLabel();
if (change_label) {
emulate_click(change_label);
}
var b = getBody(form);
b.value = grep([
T[num].body, T[0]._init.body, T[num].body_latter
], function(i) { return i; }).join("\n\n");
var selectors = x('bo,s,t');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
if ((j.name == 'body') || !j.value) {
j.focus();
j.selectionStart = 0;
j.selectionEnd = 0;
}
}
var undos = [
toOption('-------', null, null, "gts_undo_option", true),
toOption(' ' + trans("Undo"), 'undo', null, "gts_undo_option", true)
];
var gts_undos = getGtsOptUndos();
for (var i=0; i<gts_undos.length; i++) {
gts_undos[i].parentNode.removeChild(gts_undos[i]);
}
var gts_first = getGtsOptFirst();
if (gts_first) {
gts_first.parentNode.insertBefore( undos[1], gts_first.nextSibling );
gts_first.parentNode.insertBefore( undos[0], gts_first.nextSibling );
}
msg(trans('applied', T[num].name), function() {
undo(form);
msg(trans("To apply template was canceled."));
}, true);
}
function undo(form) {
if (typeof T[0]._init != 'undefined') {
var selectors = x('f,t,c,bc,bo');
for (var i in selectors) {
var j = xpath(selectors[i], form)[0];
j.value = T[0]._init[j.name];
}
}
delete T[0]._init;
var gts_undos = getGtsOptUndos();
for (var i=0; i<gts_undos.length; i++) {
gts_undos[i].parentNode.removeChild(gts_undos[i]);
}
}
function emulate_click(target) {
if (target.dispatchEvent) {
var e = unsafeWindow.document.createEvent("MouseEvents");
e.initEvent("click", true, true);
target.dispatchEvent(e);
}
}
function deleteTemplate(num , form) {
var name = T[num].name;
if (confirm(trans('remove confirm', name)) != true) {
return;
}
T[num] = {'id' : T[num].id, 'num' : num};
editContact(T[num], function() {
recomposeSelectElement(form);
msg(trans("removed", name));
});
}
function getTemplates(f_parseTemplate) {
var queryUrl = 'mail/contacts/data/contacts?thumb=false&groups=false&show=ALL&psort=Name&max=300&out=js&rf=&jsx=true';
ajax(queryUrl, function(req){
contactPage = req.responseText.replace('while (true); ', '').replace(/&&&START&&&([^&&&]+)&&&END&&&/, "$1");
response = eval("(" + contactPage + ")");
if (response.Success) {
var contacts = response.Body.Contacts;
var notes = [];
for(i=0; i<contacts.length; i++) {
if (contacts[i].Name && /gtssettings(\d)/.exec(contacts[i].Name) ) {
num = RegExp.$1;
note = contacts[i].Notes;
id = contacts[i].Id;
notes.push({'num' : num, 'note' : note, 'id' : id});
}
}
var authtoken = response.Body.AuthToken.Value;
GM_setValue(KEY_TOKEN, authtoken);
f_parseTemplate(notes);
} else {
log("Contacts Request Failed: " + response.Errors[0].Text);
}
});
}
function encode(tmpl, by_escape) {
if (by_escape) {
var escaped = {};
//encodeURIだと「'」がエンコードされないので、escapeを使う
for (var i in tmpl) {
if (i.indexOf('_') != 0) {
escaped[i] = escape(tmpl[i]);
}
}
return escaped;
} else {
//連絡先は「"」で囲まれるため、JSONデータを表すのに「"」を使えない。
//連絡先から復元するときに使うデータを、REGEXでマッチさせるため「"」を「'」に置換しておく。
return tmpl.toSource().replace(/\"/g, "'");
}
}
function decode(tmpl, by_unescape) {
if (by_unescape) {
var unescaped = {};
for (var i in tmpl) {
unescaped[i] = unescape(tmpl[i]);
}
return unescaped;
} else {
//連絡先に格納するために、「'」に変換しておいた「"」を戻す
return tmpl.replace(/\\'/g, "\"");
}
}
function editContact(tmpl, f_completed) {
var authtoken = GM_getValue(KEY_TOKEN);
if (!authtoken) {
log('token not found');
return;
}
var escaped = encode(tmpl, true);
var post_data = param({
"token" : authtoken,
"tok" : authtoken,
"out" : "js",
"id" : tmpl.id,
"action" : "SET",
"Name" : CONTACT_NAME + tmpl.num,
"Emails.0.Address" : CONTACT_NAME + tmpl.num + "@gmail.com",
"Notes" : encode(escaped, false)
});
ajax("mail/contacts/update/contact", function(req) {
var response = eval("(" + req.responseText.replace('while (true); ', '').replace(/&&&START&&&([^&&&]+)&&&END&&&/, "$1") + ")");
if (response.Success) {
if (tmpl.id == -1) {
if (CONTACT_ID_RE.exec(req.responseText)) {
tmpl.id = RegExp.$1;
editContact(tmpl, f_completed);
}
} else {
if (tmpl.num != 0) {
save(f_completed);
} else {
f_completed();
}
}
} else {
log("Update Contact Request Failed: " + response.Errors[0].Text);
}
}, 'POST', {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'}, post_data);
}
function log(message) {
if (unsafeWindow && unsafeWindow.console && DEBUG) {
unsafeWindow.console.log(message);
}
}
function getCookie(name) {
var re = new RegExp(name + "=([^;]+)");
var value = re.exec(document.cookie);
return (value != null) ? decodeURI(value[1]) : null;
}
function msg(message, f_clicked, is_undo) {
unsafeWindow.gts_undo = f_clicked;
var a = xpath(SELECTOR['msg'], getView().ownerDocument.body);
if (a && a.length > 0) {
var td = a[0];
var div = td.parentNode.parentNode.parentNode.parentNode;
div.style.visibility = "visible";
td.innerHTML = "GTS : " + message;
if (is_undo) {
var span = document.createElement('span');
span.setAttribute("id", "gts_und");
span.setAttribute("class", "lk");
span.innerHTML = trans('Undo link');
span.addEventListener('click', f_clicked, true);
td.appendChild(span);
}
window.setTimeout(function() {
div.style.visibility = "hidden";
}, 60000);
}
}
function save(f_saved) {
T[0]['num'] = 0;
T[0].date = new Date();
editContact(T[0], f_saved ? f_saved : function() {});
}
function ajax(request_path, f_load, get_or_post, headers, data) {
window.setTimeout(function() {
GM_xmlhttpRequest({
'method': get_or_post ? get_or_post : "GET",
'url': getBaseLocation() + request_path,
'data': data,
'headers': headers,
'onload': f_load,
'onerror': function(req) {
log("Request Failed in error code: " + req.status);
}
});
}, 0);
}
function getBaseLocation() {
if (LOCATION_RE.exec(document.location)) {//for Google Apps
return RegExp.$1;
} else {
return 'http://mail.google.com/';
}
}
function trans(msg_id, opt) {
return {
'Template actions...' : Ja ? 'テンプレートの操作...' : msg_id,
'Apply' : Ja ? '適用' : msg_id,
'Append' : Ja ? '追加' : msg_id,
'Includes from' : Ja ? '差出人を含む' : msg_id,
'Excludes from' : Ja ? '差出人を除く' : msg_id,
'Quantity limit is 9' : Ja ? '最大9個です' : msg_id,
'Remove' : Ja ? '削除' : msg_id,
'verbs' : Ja ? (opt + 'するテンプレート') : (opt + ' template'),
'Please input the template name.' : Ja ? 'テンプレート名を入力してください。' : msg_id,
'If the name is started from "#", it becomes default of the corresponding "from".' :
Ja ? '名前を「#」から始めると、対応する差出人のデフォルトになります。' : msg_id,
'The name already exists.' : Ja ? 'その名前は既に存在します。' : msg_id,
'appended' : Ja ? ("テンプレート「" + opt + "」を追加しました。")
: ('Template "' + opt + '" was appended.'),
'applied' : Ja ? ("テンプレート「"+opt+"」を適用しました。")
: ('Template "' + opt + '" was applied. '),
'remove confirm' : Ja ? ("テンプレート「" + opt + "」を削除しますか?")
: ('Is template "' + opt + '" removed?'),
'removed' : Ja ? ("テンプレート「" + opt + "」を削除しました。")
: ('Template "' + opt + '" was removed.'),
'To apply template was canceled.' : Ja ? "テンプレートの適用は取り消されました。" : msg_id,
'Undo' : Ja ? "適用の取り消し" : msg_id,
'Undo link' : Ja ? "適用取り消し" : "Undo applied",
'No from' : Ja ? "差出人なし" : msg_id
}[msg_id] || msg_id;
}
function x(prefix) {
var result = [];
for (var i in SELECTOR) {
if (typeof prefix != 'undefined') {
var a = grep(prefix.split(','), function(j) { return i.indexOf(j) == 0; });
if (a.length != 0) { result.push(SELECTOR[i]); }
} else {
result.push(SELECTOR[i]);
}
}
return result;
}
/*
* this 'grep' function from jquery-1.2.2.js
* jQuery 1.2.2 - New Wave Javascript
*/
function grep( elems, callback, inv ) {
// If a string is passed in for the function, make a function
// for it (a handy shortcut)
if ( typeof callback == "string" )
callback = eval("false||function(a,i){return " + callback + "}");
var ret = [];
// Go through the array, only saving the items
// that pass the validator function
for ( var i = 0, length = elems.length; i < length; i++ )
if ( !inv && callback( elems[ i ], i ) || inv && !callback( elems[ i ], i ) )
ret.push( elems[ i ] );
return ret;
}
/*
* this 'param' function from jquery-1.2.2.js
* jQuery 1.2.2 - New Wave Javascript
*/
function param(a) {
var s = [];
// Serialize the key/values
for ( var j in a )
// If the value is an array then the key names need to be repeated
s.push( encodeURIComponent(j) + "=" + encodeURIComponent( a[j] ) );
// Return the resulting serialization
return s.join("&").replace(/%20/g, "+");
}
/*
* this 'trim' function from jquery-1.2.2.js
* jQuery 1.2.2 - New Wave Javascript
*/
function trim(text) {
return (text || "").replace( /^\s+|\s+$/g, "" );
}
function xpath(query, target) {
var results = document.evaluate(query, target || document, null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
var nodes = [];
for (var i=0; i<results.snapshotLength; i++) {
nodes.push(results.snapshotItem(i));
}
return nodes;
}
gmail.registerViewChangeCallback(switcher);
switcher();
});
}
}, true);
ラベル: firefox, gmail, google, greasemonkey, javascript
2008/03/09
RubyでGoogle Data APIs
Googleが提供するさまざまなサービスには、Google Data APIsを利用して、データの取得や更新を行うことができます。
現在、以下のサービスへのAPIが提供されています。
- Google Apps APIs
- Google Base data API
- Blogger data API
- Google Calendar data API
- Google Code Search data API
- Google Notebook data API
- Google Spreadsheets data API
- Picasa Web Albums data API
- Google Documents List data API
- YouTube data API
JavaScriptからSpreadsheetsを操作できないかと調べてみたのですが、JavaScript client libraryは、現在、Google Calendar と Bloggerのみのようです。
そこで、Rubyのライブラリを探してみると、gdata-rubyというライブラリがありました。
ただ、このライブラリは現在開発がストップしているらしく、SpreadsheetとBloggerのみに対応しているようです。
というわけで、せっかくなのでRubyでGoogle Spreadsheets data APIを試してみました。
ruby-gdataのインストール
gem install GData書き込みテスト
#!/usr/bin/env ruby
require 'gdata/spreadsheet'
gdata_user = 'xxx@gmail.com'
gdata_pass = 'xxx'
gs_key = 'xxx'
gs = GData::Spreadsheet.new(gs_key)
gs.authenticate(gdata_user, gdata_pass)
gs.add_to_cell 'sin(0.2)'
残念ながら、使用感はいまいちでした。他によいライブラリはないのでしょうか・・・?
2008/02/29
Google Chart API を使ってみる
しばらく前に、Google Chart API が公開されたのですが、後れ馳せながら使ってみました。
Google Chart APIは、Google Static Maps APIと同じようにグラフを画像(PNGフォーマット)で返してくれるAPIです。(Google Chart APIが先ですが・・)
http://chart.apis.google.com/chart?cht=p3&chd=s:hW&chs=400x200&chl=Hello|World
Google Chart API のURLは、次の形式になっていて、parameters部分は以下のようになっています。※グラフの種類によって違ってきます。
http://chart.apis.google.com/chart?parameters
- chs
- グラフのサイズ
- e.g. 400x250
- ※最大値は1000x1000
- chd
- グラフのデータ
- e.g. s:helloWorld
- ※データをエンコードした文字列を設定します
- cht
- グラフの種類
- e.g. lc
- chxt
- 軸ラベルの表示
- e.g. x,y
- chxl
- 軸ラベル
- e.g. 0:|Mar|Apr|May|June|July|1:||50+Kb
上記の例だと、このようなグラフになります。
http://chart.apis.google.com/chart?chs=400x250&chd=s:helloWorld&cht=lc&chxt=x,y&chxl=0:|Mar|Apr|May|June|July|1:||50+Kb
表示できるグラフの種類は、円・棒・折れ線グラフはもちろん、面グラフや散布図といったものまで表示でき、大概のグラフは表示できそうです。
これまでグラフを貼り付けるのに、Excelで作成したものを貼り付けたり、Flashでやっていたものが、簡単に貼り付けられるようになったのは素晴らしいと思います。ただ、今のところ(2008/02/29現在)、ラベルに日本語はうまく表示できないようなのがちょっと残念。
なお、利用にあたっては、ユーザあたり50,000リクエスト/日(query limit of 50,000 queries per user per day)という制限があります。
2008/02/28
Google Static Maps APIを使ってみる
先日、より簡単にGoogleマップを表示できるようになる、Google Static Maps APIが公開されました。
特定の地図をimageデータ(GIFフォーマット)として返してくれるようで、いろいろと使い道があるのではないでしょうか。(特にモバイル)
というわけで、さっそく使ってみました。
携帯で表示(240x270)
http://www.r-stone.net/blogs/ishikawa/google_static_maps_api_mobile.html
使い方は非常に簡単で、Static Map Wizardで手軽に作成できます。
Google Static Maps API のURLは、次の形式になっていて、parameters部分は以下のようになっています。
http://maps.google.com/staticmap?parameters
- center(必須)
- マップの中央の座標
- e.g. 40.714728,-73.998672
- zoom(必須)
- ズームレベル
- e.g. 17
- size(必須)
- 画像のサイズ
- e.g. 500x400
- ※最大値は512x512
- maptype(オプション)
- roadmap(デフォルト):通常のマップ
- mobile:表示文字等が簡略化されたモバイル用
- markers(オプション)
- マーカーの位置,色,文字
- ※"|"で区切ることで複数マーカーが可能
- {latitude},{longitude},{color}{alpha-character}
- e.g. 40.702147,-74.015794,blues|40.711614,-74.012318,greeng
- key(必須)
- APIキー
- Google Maps APIと同じ
なお、利用にあたって、1ユーザ(1IPアドレス)当たり、表示は1日1,000種類の画像までという制限(※原文によると「1000 unique (different) image requests per viewer per day」)があるので要注意です。
2008/01/25
Google ブックマークのデータが消えている
どこでも共通のブックマークが使えるように、ブックマークにはGoogle ブックマークを使っているのですが、今朝(2008/01/25現在)アクセスしてみると、いくつかのブックマークを除き、ほとんどが消えてしまっています。
はじめは、自分の誤操作かとも疑いましたが、ここ最近のものを除いたすべてが消えているようです。
これはおかしいと思い調べてみると、同様の現象に見舞われている人がいるようです。
Google ツールバーのブックマークが消えた -OKWave
昨晩まで使えていた Google ツールバー のブックマークが今朝、ただひとつのブックマーク(直近に追加したもの)だけを残して消えていました(同じツールバーの検索タイプの方は登録内容が残っています)。
ツールバーの[ブックマーク]−[更新]を何度か試みましたが復活しません。登録内容はエクスポートしていません。元に戻す方法ないでしょうか?
上記サイトにあるように、僕の場合もGoogle Notebookにデータ自体あったのですが、手動での復旧は億劫なので、Googleの対応を待ったほうがよさそうです・・・。
データがどんどんネットに保存されて便利になっていく一方で、それが壊れたときの衝撃はすさまじいものだと痛感しました。
サービス提供者として、肝に銘じておかなければなりません。
Update:
既に復活しているようです。夕方くらいには復活したようですね。
ラベル: google

