Twitter to Google Talk bridge
TwitterをGmailのWebインターフェイス上のGoogle Talkで読み書きできるといいなあと思い,調べてみたところ,Twitterには昔はJabber(XMPP)インターフェイスがあったが,ある時点からなくなったらしい.ちょうどいいので,App Engineで作ってみた.
設計
Twitter からGoogle Talkへのブリッジには,cronを用いる.定期的に起動するサーブレットが,Twitterのfriends time lineを取得し,更新があれば,Google Talkへxmppへ送信する.この際,前回送信した最後のメッセージIDを保存しておく必要がある.
Google TalkからTwitterへのブリッジは,XMPPハンドラで,送信元のXMPPアドレスから,フォワードするべきTwitterアカウントを判断し,そのアカウントにフォワードする.
データ構造
ユーザごとに,XMPPのアドレスと,Twitterのアカウント名,パスワード,最後に読み出したメッセージのIDを保持する必要がある.クラス定義は下記のようになる.
import javax.jdo.annotations.*; @PersistenceCapable(identityType = IdentityType.APPLICATION) public class TwitterUser { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Long id; @Persistent private String xmppAddr; @Persistent private String username; @Persistent private String password; @Persistent private long lastId; public TwitterUser(String xmppAddr, String username, String password, long lastId) { super(); this.xmppAddr = xmppAddr; this.username = username; this.password = password; this.lastId = lastId; } // ... getters / setters 省略 }
TwitterからGoogle Talkへ
cronから起動されるサーブレットは
- データストアからTwitterUser Kindのオブジェクトをすべて取り出し,それぞれに対して,
- friends timelineの更新を取得し,
- 個々のメッセージをXMPPで送信する.
コードはこんな感じ.friends timeline は最大でも1ページ分しか取得していない.デフォルトでは1ページは20メッセージなので,このロジックでは,人によっては取りこぼすこともあるだろう.
@SuppressWarnings("serial") public class TwitterToXMPPServlet extends HttpServlet { @Override public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { PersistenceManager pm = PMF.get().getPersistenceManager(); Query query = pm.newQuery(TwitterUser.class); List<TwitterUser> users = (List<TwitterUser>) query.execute(); for (TwitterUser user: users) { String to = user.getXmppAddr(); String username = user.getUsername(); String password = user.getPassword(); long lastId = user.getLastId(); JID jid = new JID(to); Twitter twitter = new Twitter(username, password); List<Status> statuses = null; try { statuses = twitter.getFriendsTimeline(new Paging(1).sinceId(lastId)); } catch (TwitterException e) { e.printStackTrace(); System.err.println("failed to get status for user: " + username); continue; } Collections.reverse(statuses); for (Status status: statuses){ String mes = status.getUser().getName() + ":" + status.getText(); if (! sendXMPPMessage(jid, mes, System.err)) break; lastId = status.getId(); } user.setLastId(lastId); // update lastId try { pm.makePersistent(user); } finally { pm.close(); } } } ... }
送信するメソッドはこんな感じ.送信に失敗すると,falseを返す.falseが返った場合には,次回cronで起動された際に再送信が試みられる.
private boolean sendXMPPMessage(JID jid, String msgBody, PrintStream writer) { boolean messageSent = false; Message msg = new MessageBuilder() .withRecipientJids(jid) .withBody(msgBody) .build(); XMPPService xmpp = XMPPServiceFactory.getXMPPService(); if (xmpp.getPresence(jid).isAvailable()) { SendResponse status = xmpp.sendMessage(msg); messageSent = (status.getStatusMap().get(jid) == SendResponse.Status.SUCCESS); if (messageSent) { writer.println("<h2>Message Sent!</h2>"); return true; } else{ writer.println("<h2>Failed!</h2>"); return false; } } else { writer.println("<h2>the address " + jid.getId() + " is not available</h2>"); return false; } }
cronをセットするには,war/WEB-INF/cron.xmlを書けばよい.
<?xml version="1.0" encoding="UTF-8"?> <cronentries> <cron> <url>/t2x</url> <schedule>every 5 minutes</schedule> </cron> </cronentries>
/t2x に先のサーブレットを登録している.
Google TalkからTwitterへ
- 送信者のXMPPアドレスから,TwitterUserオブジェクトを検索
- TwitterUser に送信
split("/")しているのは,Jid.getId()で返されるアドレスの後部に余分な文字(?)が付いているので.
public class XMPPToTwitterServlet extends HttpServlet { public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException { XMPPService xmpp = XMPPServiceFactory.getXMPPService(); Message message = xmpp.parseMessage(req); String xmppAddr = message.getFromJid().getId().split("/")[0].toLowerCase(); PersistenceManager pm = PMF.get().getPersistenceManager(); Query query = pm.newQuery(TwitterUser.class); query.setFilter("xmppAddr == ADDR"); query.declareParameters("java.lang.String ADDR"); List<TwitterUser> users = (List<TwitterUser>) query.execute(xmppAddr); if (users.isEmpty()) { System.err.println("no twitter user found for " + xmppAddr); System.err.println("body: "+ message.getBody()); return; } TwitterUser user = users.get(0); Twitter twitter = new Twitter(user.getUsername(), user.getPassword()); try { twitter.updateStatus(message.getBody()); } catch (TwitterException e) { e.printStackTrace(); } } }
もちろん,このサーブレットをweb.xmlで,/_ah/xmpp/message/chat/にマップする必要がある.
<servlet> <servlet-name>xmpp2twitter</servlet-name> <servlet-class>XMPPToTwitterServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>xmpp2twitter</servlet-name> <url-pattern>/_ah/xmpp/message/chat/</url-pattern> </servlet-mapping>
これで,Google Talkで書くだけで,Twitterにポストすることができる.
問題点
実は,今のところ,Google TalkからTwitterに送信するほうは,マルチバイトコードで動かない.これはApp EngineのXMPP受信機能の既知のバグで1.2.6で直る,らしい.まあ,まだ新しい機能なので仕方がないか.
所感
XMPPとApp Engineの組み合わせは強力で,いろいろと使い道がありそうだ.それだけに,このバグは残念.早く直らないかな...
追記 (10/14)
1.2.6でマルチバイトが通るようになった.すばらしや.