Author Archives: susumuis

About susumuis

メイドカフェによく居るWebエンジニア

俺タイムラインメールアプリ

前回(といっても、半年近く前なんですね) http://d.hatena.ne.jp/s-ishigami/20100224 で、TwitterのTimeLineや、Replyをメール配信するGAE/Java アプリを公開させていただきました。

その後、構造化されていないとか、一部からご反響をいただきましたが、あくまでサンプルコードのつもりで気楽に書いているので、メソッド一個でがんばりました。
Slim3とかJDOとか使っていません。

さて、この度
appengine ja hack-a-thon #6 (http://atnd.org/events/5932)
に参加させていただき、多くのすばらしい先生方を尻目に初心者の私ですが、上記アプリを拡張させていただいた次第でございます。

今回実装したのは以下のとおりです。
・OAuth対応
・DirectMessage対応

それではコードです。

package s_ishigami.ore_timeline_mail;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import twitter4j.DirectMessage;
import twitter4j.Paging;
import twitter4j.ResponseList;
import twitter4j.Status;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.http.AccessToken;
import twitter4j.http.RequestToken;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.mail.MailServiceFactory;
import com.google.appengine.api.mail.MailService.Message;
import com.google.appengine.api.memcache.Expiration;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
@SuppressWarnings("serial")
public class OreTimelineMailServlet extends HttpServlet {
// 設定情報
private static final String SYSTEM_ADMIN_MAIL = "xxxx@xxx.xx";
private static final String YOUR_MAIL = "xxxx@xxxx.xx";
private static final String CONSUMER_KEY = "XXXXXXX";
private static final String CONSUMER_SECRET = "XXXXXXXXXXXXXXXXXXXXXX";
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
String path = request.getRequestURI().substring(request.getContextPath().length());
DatastoreService dataService = DatastoreServiceFactory.getDatastoreService();
MemcacheService memcacheService = MemcacheServiceFactory.getMemcacheService();
try {
Twitter twitter = new TwitterFactory().getOAuthAuthorizedInstance(CONSUMER_KEY, CONSUMER_SECRET);
// OAuth認証コールバック
if (path.equals("/oauth_recieve")) {
RequestToken requestToken = (RequestToken) memcacheService.get("oauth_request_token");
if (requestToken == null) {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
response.getWriter().println("多分timeout");
return;
}
AccessToken accessToken = twitter.getOAuthAccessToken(requestToken);
Entity entity = new Entity("twitter", "oauth_access_token");
entity.setProperty("token", accessToken.getToken());
entity.setProperty("tokenSecret", accessToken.getTokenSecret());
dataService.put(entity);
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
response.getWriter().println("OAuth 認証を登録しました。");
return;
}
// OAuth認証
AccessToken accessToken = null;
try {
Entity entity = dataService.get(KeyFactory.createKey("twitter", "oauth_access_token"));
accessToken = new AccessToken((String) entity.getProperty("token"), (String) entity.getProperty("tokenSecret"));
} catch (EntityNotFoundException e) {}
if (accessToken == null) {
// 認証済みでない場合は、リクエストトークンを発行してキャッシュに保持・リダイレクト
RequestToken requestToken = twitter.getOAuthRequestToken();
memcacheService.put("oauth_request_token", requestToken, Expiration.byDeltaSeconds(600)); // 10分有効
response.sendRedirect(requestToken.getAuthorizationURL());
return;
}
twitter.setOAuthAccessToken(accessToken);
// 以下アプリ
StringBuilder text = new StringBuilder();
if (path.equals("/clear")) {
Entity entity = new Entity("twitter", "timeline");
entity.setProperty("lastId", -1);
dataService.put(entity);
entity = new Entity("twitter", "reply");
entity.setProperty("lastId", -1);
dataService.put(entity);
entity = new Entity("twitter", "direct");
entity.setProperty("lastId", -1);
dataService.put(entity);
}
if (path.equals("/tl")) {
// 保存された最新のIDを取得
long lastId = -1;
try {
Object id = dataService.get(KeyFactory.createKey("twitter", "timeline")).getProperty("lastId");
lastId = (id == null ? lastId : ((Long) id).longValue());
} catch (EntityNotFoundException e) {}
ResponseList<Status> timeline = twitter.getHomeTimeline();
for (Status status : timeline) {
if (lastId == status.getId()) { break; }
text.append(status.getUser().getScreenName() + ":" + status.getText() + "\n");
}
if (!"true".equals(request.getParameter("not_save"))) {
// 最新のIDを保存
long theLastId = timeline.get(0).getId();
Entity entity = new Entity("twitter", "timeline");
entity.setProperty("lastId", theLastId);
dataService.put(entity);
}
} else if (path.equals("/reply")) {
// 保存された最新のIDを取得
long lastId = -1;
try {
Object id = dataService.get(KeyFactory.createKey("twitter", "reply")).getProperty("lastId");
lastId = (id == null ? lastId : ((Long) id).longValue());
} catch (EntityNotFoundException e) {}
ResponseList<Status> mentions = twitter.getMentions(new Paging(1, 8));
for (Status status : mentions) {
if (lastId == status.getId()) {
break;
}
text.append(status.getUser().getScreenName() + ":" + status.getText() + "\n");
}
if (!"true".equals(request.getParameter("not_save"))) {
// 最新のIDを保存
long theLastId = mentions.get(0).getId();
Entity entity = new Entity("twitter", "reply");
entity.setProperty("lastId", theLastId);
dataService.put(entity);
}
} else if (path.equals("/direct")) {
// 保存された最新のIDを取得
long lastId = -1;
try {
Object id = dataService.get(KeyFactory.createKey("twitter", "direct")).getProperty("lastId");
lastId = (id == null ? lastId : ((Long) id).longValue());
} catch (EntityNotFoundException e) {}
ResponseList<DirectMessage> messages = twitter.getDirectMessages(new Paging(1, 8));
for (DirectMessage message : messages) {
if (lastId == message.getId()) {
break;
}
text.append(message.getSender().getScreenName() + ":" + message.getText() + "\n");
}
if (!"true".equals(request.getParameter("not_save"))) {
// 最新のIDを保存
long theLastId = messages.get(0).getId();
Entity entity = new Entity("twitter", "direct");
entity.setProperty("lastId", theLastId);
dataService.put(entity);
}
}
if ("true".equals(request.getParameter("ispc"))) {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
response.getWriter().println(text.toString());
} else {
if (text.length() > 0) {
Message message = new Message();
message.setSubject("Twitter Timeline");
message.setTo(YOUR_MAIL);
message.setSender(SYSTEM_ADMIN_MAIL);
message.setTextBody(text.toString());
MailServiceFactory.getMailService().send(message);
}
}
} catch (TwitterException e) {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
e.printStackTrace(response.getWriter());
}
}
}

使い方
・自分用のApplicationIDを取得
・事前に http://twitter.com/oauth_clients/new でアプリを登録
  ・この際、戻りURLは、"http://[取得したAppID].appspot.com/oauth_recieve" を設定してください。
   あとは環境に合わせて。
・出力された、CONSUMER_KEYと、CONSUMER_SECRETをソースにコピペ
・メール送信元アドレス(開発者メールアドレス)と、送信先アドレス(携帯アドレス)をソースに記入
・デプロイ

PCでも携帯でもいいので、/以外にアクセス/hogeとかでも可
あとは画面に従えば認証が行われます。

twitterを携帯メールに転送するGAEアプリ

iPhoneも、Androidも、そもそも自宅ネット環境もない私はもっぱら、携帯でTwitterを使っています。

tmitter(http://tmitter.org/)でタイムラインが取得できなくなってしまったので、GAEの勉強もかねて作ってみました。

package ishigami.twittermail;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.mail.MailServiceFactory;
import com.google.appengine.api.mail.MailService.Message;
import twitter4j.Paging;
import twitter4j.ResponseList;
import twitter4j.Status;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
@SuppressWarnings("serial")
public class TwitterMailServlet extends HttpServlet {
// 設定情報
private static final String SYSTEM_ADMIN_MAIL = "XXXXXXXXXXXXXXXXXXXX";
private static final String YOUR_MAIL = "XXXXXXXXXXXXXXXXXXX";
private static final String YOUR_MAIL_POST_ADDRESS = "";
private static final String TWITTER_ID = "XXXXXXXXXXXXX";
private static final String TWITTER_PW = "XXXXXXXXXXXXX";
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
String path = request.getRequestURI().substring(request.getContextPath().length());
if (path.equals("/tl") || path.equals("/reply")) {
DatastoreService dataService = DatastoreServiceFactory.getDatastoreService();
StringBuilder text = new StringBuilder();
Twitter twitter = new TwitterFactory().getInstance(TWITTER_ID, TWITTER_PW);
if (path.equals("/tl")) {
// 保存された最新のIDを取得
long lastId = -1;
try {
Entity entity = dataService.get(KeyFactory.createKey("twitter", "timeline"));
Object id = entity.getProperty("lastId");
if (id != null) {
lastId = (Long) id;
}
} catch (EntityNotFoundException e) {
}
try {
ResponseList<Status> timeline = twitter.getHomeTimeline();
for (Status status : timeline) {
if (lastId == status.getId()) {
break;
}
text.append(status.getUser().getScreenName() + ":" + status.getText() + "\n");
}
// 最新のIDを保存
long theLastId = timeline.get(0).getId();
Entity entity = new Entity("twitter", "timeline");
entity.setProperty("lastId", theLastId);
dataService.put(entity);
} catch (TwitterException e) {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
response.getWriter().println("twitter error" + e.getMessage());
}
} else if (path.equals("/reply")) {
// 保存された最新のIDを取得
long lastId = -1;
try {
Entity entity = dataService.get(KeyFactory.createKey("twitter", "reply"));
Object id = entity.getProperty("lastId");
if (id != null) {
lastId = (Long) id;
}
} catch (EntityNotFoundException e) {
}
try {
ResponseList<Status> mentions = twitter.getMentions(new Paging(1, 8));
for (Status status : mentions) {
if (lastId == status.getId()) {
break;
}
text.append(status.getUser().getScreenName() + ":" + status.getText() + "\n");
}
// 最新のIDを保存
long theLastId = mentions.get(0).getId();
Entity entity = new Entity("twitter", "reply");
entity.setProperty("lastId", theLastId);
dataService.put(entity);
} catch (TwitterException e) {
throw new RuntimeException(e);
}
if (text.length() > 0) {
text.append("\n" + YOUR_MAIL_POST_ADDRESS);
}
}
if (text.length() > 0) {
Message message = new Message();
message.setSubject("Twitter Timeline");
message.setTo(YOUR_MAIL);
message.setSender(SYSTEM_ADMIN_MAIL);
message.setTextBody(text.toString());
MailServiceFactory.getMailService().send(message);
}
}
}
}

web.xml

<web-app>
<servlet>
<servlet-name>TwitterMail</servlet-name>
<servlet-class>ishigami.TwitterMailServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TwitterMail</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>

cronで、定期的に"/reply"をリクエストするように設定しとくと、
自分にメッセージが来たときに携帯にメールが来るので便利です。

自分用なので、荒削りでごめんなさい。

取り扱いについては、煮るなり焼くなり好きにしろですが、保証は一切いたしません。
使いたい方は自分でGAEのアカウントを取得して、上の方の定数に適当な値を入れてください。

見て分かる通り、ソースにIDとかパスワードとかべた書きなので、
そう言うのが気に入らない人は使わないようにお願いします:D

Mayaa ファイルの共通化

 同じ機能のテンプレートが複数セット存在する場合を考えます。例えば、/1/A.htmlと、/2/A.htmlは、デザインが違いますが、全く同じ機能だとします。このとき、/1/A.mayaa を /2/A.mayaa に複製するのが基本です。しかし、それだと、/1/A.mayaaを修正したときは、同時に/2/A.mayaaも修正しなくてはなりません。/3/A.mayaa、/4/A.mayaaのように、さらに複製されていた場合はなおさら厄介です。
 
 複数のテンプレートが共通のMayaaファイル(Specファイル?)を参照する場合、手っ取り早い方法としては、templateSuffixを使う方法があります。
http://mayaa.seasar.org/documentation/template_suffix.html
 しかし、上記のようにフォルダでテンプレートをグループ分けしたい場合は、suffixは使いにくいです。

 シンボリックリンクによる方法も簡単です。これなら、Mayaaのカスタマイズなしに、すぐに使えます。しかし、Windows上でテストできない点が私はネックだと思いました。Javaシステムなので、OSやサーバに依存しないで解決したいです。

 この度、Mayaa ユーザメーリングリストにて、次のように質問させていただきました。
 http://ml.seasar.org/archives/mayaa-user/2009-December/000868.html
 suga様の回答(ありがとうございます。)を参考に、SourceDescriptorのカスタマイズによって解決しました。以下ソースを貼っておきます。

package mypackage;
import java.io.InputStream;
import java.util.Date;
import org.seasar.mayaa.impl.source.PageSourceDescriptor;
import org.seasar.mayaa.source.SourceDescriptor;
public class MyPageSourceDescriptor extends PageSourceDescriptor {
// ルートのmayaaファイルを探しに行くディスクリプタ
private SourceDescriptor rootDescriptor = new PageSourceDescriptor();
// 注意:SystemIDはmayaaファイルの絶対パス
/**
    * ソースSystemIDを設定する。
    * 
    * @param systemID
    */
@Override
public void setSystemID(String systemID) {
super.setSystemID(systemID);
// ルートのパス取得。
String rootId = getRootId(systemID);
if (rootId != null) {
rootDescriptor.setSystemID(rootId);
} else {
rootDescriptor.setSystemID(systemID);
}
}
// 要求パスからルートのパスを解決する。
private String getRootId(String systemId) {
if (systemId.startsWith("/client_info/")) {
int pt = systemId.indexOf("/", "/client_info/".length());
if (pt != -1) {
return systemId.substring(pt);
}
}
return null;
}
/**
    * ソースが存在するかどうかを取得する。
    * 
    * @return ファイルが存在すればtrue。無ければfalse。
    */
@Override
public boolean exists() {
// ルートの方が見つかりやすいから、処理高速化のためルート優先
return rootDescriptor.exists() || super.exists();
}
/**
    * ファイルのインプットストリームを取得する。
    * 
    * @return ストリーム。もしファイルが無い場合は、null。
    */
@Override
public InputStream getInputStream() {
InputStream result = super.getInputStream();
if (result == null) {
result = rootDescriptor.getInputStream();
}
return result;
}
private static final Date nullDate = new Date(0);
/**
    * ファイルの日付を取得する。
    * 
    * @return ファイルの最終更新日付。ファイルが無い場合は「new Date(0)」を返す。
    */
@Override
public Date getTimestamp() {
Date result = super.getTimestamp();
if (result.equals(nullDate)) {
result = rootDescriptor.getTimestamp();
}
return result;
}
}

 このようにラッパーにすることで、外側からは完全に隠蔽しつつ、内部で代替ロジックを動かすことができます。教科書的に言えばMyPageSourceDescriptor はSourceDescriptorをimplementsして、PaseSourceDescriptorをもう一つフィールドとして持ち、メソッドをそちらに移譲するようにした方が、構造がわかりやすいでしょう。しかし、それだとコード量が増えてしまうので、わかった上で継承で代用することはありだと思ってます。

 これを動かしたところ、うまくユーザごとのMayaaファイルが存在するときはそちらを、存在しない時は、ルートのMayaaファイルを処理するようになりました。

 また、この修正の副作用として、ユーザのテンプレートが存在しない場合に、ルートのテンプレートを探すようになりました。内部的にinsertしている場合も、うまいこと処理してくれるので、Mayaa内部で賢いことをしてくれているのだと思います。
 勘違いしてました。SourceDescrptorは、mayaaファイルだけでなく、htmlテンプレートも探しに行くようです。私としては大変都合が良いことでしたが、勘違いしないように気を付けた方が良さそうですね。